diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..b3e96becb --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,4 @@ +{ + "version": "0.2.0", + "configurations": [] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..56c4b8b72 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,4 @@ +{ + "tasks": [], + "version": "2.0.0" +} \ No newline at end of file diff --git a/Android/APIExample/.gitignore b/Android/APIExample/.gitignore new file mode 100644 index 000000000..23d57e4d8 --- /dev/null +++ b/Android/APIExample/.gitignore @@ -0,0 +1,18 @@ +*.so +*.iml +.gradle +/local.properties +/.idea +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +androidTest/ +Test/ diff --git a/Android/APIExample/README.md b/Android/APIExample/README.md new file mode 100644 index 000000000..7e45e247e --- /dev/null +++ b/Android/APIExample/README.md @@ -0,0 +1,48 @@ +# API Example Android + +*English | [中文](README.zh.md)* + +This project presents you a set of API examples to help you understand how to use Agora APIs. + +## Prerequisites + +- Android Studio 3.0+ +- Physical Android device +- Android simulator is supported + +## Quick Start + +This section shows you how to prepare, build, and run the sample application. + +### Obtain an App Id + +To build and run the sample application, get an App Id: + +1. Create a developer account at [agora.io](https://dashboard.agora.io/signin/). Once you finish the signup process, you will be redirected to the Dashboard. +2. Navigate in the Dashboard tree on the left to **Projects** > **Project List**. +3. Save the **App Id** from the Dashboard for later use. +4. Generate a temp **Access Token** (valid for 24 hours) from dashboard page with given channel name, save for later use. + +5. Open `Android/APIExample` and edit the `app/src/main/res/values/string-config.xml` file. Update `<#Your App Id#>` with your App Id, and change `<#Temp Access Token#>` with the temp Access Token generated from dashboard. Note you can leave the token variable `null` if your project has not turned on security token. + + ``` + YOUR APP ID + // assign token to null if you have not enabled app certificate + YOUR ACCESS TOKEN + ``` + +You are all set. Now connect your Android device and run the project. + +## Contact Us + +- For potential issues, take a look at our [FAQ](https://docs.agora.io/en/faq) first +- Dive into [Agora SDK Samples](https://github.com/AgoraIO) to see more tutorials +- Take a look at [Agora Use Case](https://github.com/AgoraIO-usecase) for more complicated real use case +- Repositories managed by developer communities can be found at [Agora Community](https://github.com/AgoraIO-Community) +- You can find full API documentation at [Document Center](https://docs.agora.io/en/) +- If you encounter problems during integration, you can ask question in [Stack Overflow](https://stackoverflow.com/questions/tagged/agora.io) +- You can file bugs about this sample at [issue](https://github.com/AgoraIO/Basic-Video-Call/issues) + +## License + +The MIT License (MIT) diff --git a/Android/APIExample/README.zh.md b/Android/APIExample/README.zh.md new file mode 100644 index 000000000..e322ea18d --- /dev/null +++ b/Android/APIExample/README.zh.md @@ -0,0 +1,49 @@ +# API Example Android + +*[English](README.md) | 中文* + +这个开源示例项目演示了Agora视频SDK的部分API使用示例,以帮助开发者更好地理解和运用Agora视频SDK的API。 + +## 环境准备 + +- Android Studio 3.0+ +- Android 真机设备 +- 支持模拟器 + +## 运行示例程序 + +这个段落主要讲解了如何编译和运行实例程序。 + +### 创建Agora账号并获取AppId + +在编译和启动实例程序前,你需要首先获取一个可用的App Id: + +1. 在[agora.io](https://dashboard.agora.io/signin/)创建一个开发者账号 +2. 前往后台页面,点击左部导航栏的 **项目 > 项目列表** 菜单 +3. 复制后台的 **App Id** 并备注,稍后启动应用时会用到它 +4. 在项目页面生成临时 **Access Token** (24小时内有效)并备注,注意生成的Token只能适用于对应的频道名。 + +5. 打开 `Android/APIExample` 并编辑 `app/src/main/res/values/string-config.xml`,将你的 AppID 和 Token 分别替换到 `<#Your App Id#>` 与 `<#Temp Access Token#>` + + ``` + YOUR APP ID + // 如果你没有打开Token功能,token可以直接给null或者不填 + YOUR ACCESS TOKEN + ``` + +然后你就可以编译并运行项目了。 + +## 联系我们 + +- 如果你遇到了困难,可以先参阅 [常见问题](https://docs.agora.io/cn/faq) +- 如果你想了解更多官方示例,可以参考 [官方SDK示例](https://github.com/AgoraIO) +- 如果你想了解声网SDK在复杂场景下的应用,可以参考 [官方场景案例](https://github.com/AgoraIO-usecase) +- 如果你想了解声网的一些社区开发者维护的项目,可以查看 [社区](https://github.com/AgoraIO-Community) +- 完整的 API 文档见 [文档中心](https://docs.agora.io/cn/) +- 若遇到问题需要开发者帮助,你可以到 [开发者社区](https://rtcdeveloper.com/) 提问 +- 如果需要售后技术支持, 你可以在 [Agora Dashboard](https://dashboard.agora.io) 提交工单 +- 如果发现了示例代码的 bug,欢迎提交 [issue](https://github.com/AgoraIO/Basic-Video-Call/issues) + +## 代码许可 + +The MIT License (MIT) diff --git a/Android/APIExample/app/.gitignore b/Android/APIExample/app/.gitignore new file mode 100644 index 000000000..da62f148c --- /dev/null +++ b/Android/APIExample/app/.gitignore @@ -0,0 +1,20 @@ +*.iml +.gradle +/local.properties +/.idea +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +gradle +gradlew +gradlew.bat +.externalNativeBuild +.cxx +androidTest/ +Test/ diff --git a/Android/APIExample/app/build.gradle b/Android/APIExample/app/build.gradle new file mode 100644 index 000000000..bfc7d8b84 --- /dev/null +++ b/Android/APIExample/app/build.gradle @@ -0,0 +1,63 @@ +apply plugin: 'com.android.application' +apply plugin: "androidx.navigation.safeargs" + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.2" + + defaultConfig { + applicationId "io.agora.api.example" + minSdkVersion 19 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + jniLibs.srcDirs = ['src/main/jniLibs'] + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) + + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + + // Java language implementation + implementation "androidx.navigation:navigation-fragment:2.3.0-alpha06" + implementation "androidx.navigation:navigation-ui:2.3.0-alpha06" + + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.recyclerview:recyclerview:1.1.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + + implementation 'io.github.luizgrp.sectionedrecyclerviewadapter:sectionedrecyclerviewadapter:1.2.0' + implementation 'com.yanzhenjie:permission:2.0.3' + implementation 'de.javagl:obj:0.2.1' + implementation 'com.google.ar:core:1.0.0' + + implementation project(path: ':lib-stream-encrypt') + implementation project(path: ':lib-push-externalvideo') + implementation project(path: ':lib-raw-data') + implementation project(path: ':lib-switch-external-video') + implementation project(path: ':lib-screensharing') +} diff --git a/Android/APIExample/app/libs/RtcChannelPublishHelper.aar b/Android/APIExample/app/libs/RtcChannelPublishHelper.aar new file mode 100644 index 000000000..06c415d87 Binary files /dev/null and b/Android/APIExample/app/libs/RtcChannelPublishHelper.aar differ diff --git a/Android/APIExample/app/proguard-rules.pro b/Android/APIExample/app/proguard-rules.pro new file mode 100644 index 000000000..f1b424510 --- /dev/null +++ b/Android/APIExample/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/Android/APIExample/app/src/androidTest/java/io/agora/api/example/ExampleInstrumentedTest.java b/Android/APIExample/app/src/androidTest/java/io/agora/api/example/ExampleInstrumentedTest.java new file mode 100644 index 000000000..26eca4eba --- /dev/null +++ b/Android/APIExample/app/src/androidTest/java/io/agora/api/example/ExampleInstrumentedTest.java @@ -0,0 +1,27 @@ +package io.agora.api.example; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("io.agora.api.example", appContext.getPackageName()); + } +} diff --git a/Android/APIExample/app/src/main/AndroidManifest.xml b/Android/APIExample/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..bc86b8d59 --- /dev/null +++ b/Android/APIExample/app/src/main/AndroidManifest.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/APIExample/app/src/main/assets/effectA.wav b/Android/APIExample/app/src/main/assets/effectA.wav new file mode 100644 index 000000000..dc31fdb68 Binary files /dev/null and b/Android/APIExample/app/src/main/assets/effectA.wav differ diff --git a/Android/APIExample/app/src/main/assets/music_1.m4a b/Android/APIExample/app/src/main/assets/music_1.m4a new file mode 100644 index 000000000..3fb0b5ba5 Binary files /dev/null and b/Android/APIExample/app/src/main/assets/music_1.m4a differ diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/ExampleActivity.java b/Android/APIExample/app/src/main/java/io/agora/api/example/ExampleActivity.java new file mode 100644 index 000000000..3719c5a0a --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/ExampleActivity.java @@ -0,0 +1,186 @@ +package io.agora.api.example; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.MenuItem; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; + +import io.agora.api.component.Constant; +import io.agora.api.example.common.model.ExampleBean; +import io.agora.api.example.examples.advanced.ARCore; +import io.agora.api.example.examples.advanced.AdjustVolume; +import io.agora.api.example.examples.advanced.ChannelEncryption; +import io.agora.api.example.examples.advanced.CustomRemoteVideoRender; +import io.agora.api.example.examples.advanced.GeoFencing; +import io.agora.api.example.examples.advanced.HostAcrossChannel; +import io.agora.api.example.examples.advanced.InCallReport; +import io.agora.api.example.examples.advanced.JoinMultipleChannel; +import io.agora.api.example.examples.advanced.LiveStreaming; +import io.agora.api.example.examples.advanced.MediaPlayerKit; +import io.agora.api.example.examples.advanced.PlayAudioFiles; +import io.agora.api.example.examples.advanced.PreCallTest; +import io.agora.api.example.examples.advanced.ProcessAudioRawData; +import io.agora.api.example.examples.advanced.ProcessRawData; +import io.agora.api.example.examples.advanced.PushExternalVideo; +import io.agora.api.example.examples.advanced.SendDataStream; +import io.agora.api.example.examples.advanced.SetVideoProfile; +import io.agora.api.example.examples.advanced.SuperResolution; +import io.agora.api.example.examples.advanced.SwitchExternalVideo; +import io.agora.api.example.examples.advanced.SetAudioProfile; +import io.agora.api.example.examples.advanced.MultiProcess; +import io.agora.api.example.examples.advanced.VideoQuickSwitch; +import io.agora.api.example.examples.advanced.RTMPInjection; +import io.agora.api.example.examples.advanced.RTMPStreaming; +import io.agora.api.example.examples.advanced.StreamEncrypt; +import io.agora.api.example.examples.advanced.SwitchCameraScreenShare; +import io.agora.api.example.examples.advanced.VideoMetadata; +import io.agora.api.example.examples.advanced.VoiceEffects; +import io.agora.api.example.examples.advanced.customaudio.CustomAudioSource; +import io.agora.api.example.examples.basic.JoinChannelAudio; +import io.agora.api.example.examples.basic.JoinChannelVideo; + +/** + * @author cjw + */ +public class ExampleActivity extends AppCompatActivity { + private static final String TAG = "ExampleActivity"; + + private ExampleBean exampleBean; + + public static void instance(Activity activity, ExampleBean exampleBean) { + Intent intent = new Intent(activity, ExampleActivity.class); + intent.putExtra(Constant.DATA, exampleBean); + activity.startActivity(intent); + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_example_layout); + exampleBean = getIntent().getParcelableExtra(Constant.DATA); + + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + actionBar.setTitle(exampleBean.getName()); + actionBar.setHomeButtonEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(true); + } + + Fragment fragment; + switch (exampleBean.getActionId()) { + case R.id.action_mainFragment_to_joinChannelAudio: + fragment = new JoinChannelAudio(); + break; + case R.id.action_mainFragment_to_joinChannelVideo: + fragment = new JoinChannelVideo(); + break; + case R.id.action_mainFragment_to_CustomAudioSource: + fragment = new CustomAudioSource(); + break; + case R.id.action_mainFragment_to_CustomRemoteRender: + fragment = new CustomRemoteVideoRender(); + break; + case R.id.action_mainFragment_to_ProcessRawData: + fragment = new ProcessRawData(); + break; + case R.id.action_mainFragment_to_PushExternalVideo: + fragment = new PushExternalVideo(); + break; + case R.id.action_mainFragment_to_QuickSwitch: + fragment = new VideoQuickSwitch(); + break; + case R.id.action_mainFragment_to_MultiChannel: + fragment = new JoinMultipleChannel(); + break; + case R.id.action_mainFragment_to_SetAudioProfile: + fragment = new SetAudioProfile(); + break; + case R.id.action_mainFragment_to_PlayAudioFiles: + fragment = new PlayAudioFiles(); + break; + case R.id.action_mainFragment_to_VoiceEffects: + fragment = new VoiceEffects(); + break; + case R.id.action_mainFragment_to_MediaPlayerKit: + fragment = new MediaPlayerKit(); + break; + case R.id.action_mainFragment_to_GeoFencing: + fragment = new GeoFencing(); + break; + case R.id.action_mainFragment_to_RTMPInjection: + fragment = new RTMPInjection(); + break; + case R.id.action_mainFragment_to_RTMPStreaming: + fragment = new RTMPStreaming(); + break; + case R.id.action_mainFragment_to_StreamEncrypt: + fragment = new StreamEncrypt(); + break; + case R.id.action_mainFragment_to_SwitchExternalVideo: + fragment = new SwitchExternalVideo(); + break; + case R.id.action_mainFragment_to_SwitchCameraScreenShare: + fragment = new SwitchCameraScreenShare(); + break; + case R.id.action_mainFragment_to_VideoMetadata: + fragment = new VideoMetadata(); + break; + case R.id.action_mainFragment_to_InCallReport: + fragment = new InCallReport(); + break; + case R.id.action_mainFragment_to_AdjustVolume: + fragment = new AdjustVolume(); + break; + case R.id.action_mainFragment_to_PreCallTest: + fragment = new PreCallTest(); + break; + case R.id.action_mainFragment_to_hostacrosschannel: + fragment = new HostAcrossChannel(); + break; + case R.id.action_mainFragment_to_superResolution: + fragment = new SuperResolution(); + break; + case R.id.action_mainFragment_to_set_video_profile: + fragment = new SetVideoProfile(); + break; + case R.id.action_mainFragment_to_channel_encryption: + fragment = new ChannelEncryption(); + break; + case R.id.action_mainFragment_to_two_process_screen_share: + fragment = new MultiProcess(); + break; + case R.id.action_mainFragment_to_live_streaming: + fragment = new LiveStreaming(); + break; + case R.id.action_mainFragment_arcore: + fragment = new ARCore(); + break; + case R.id.action_mainFragment_senddatastream: + fragment = new SendDataStream(); + break; + case R.id.action_mainFragment_raw_audio: + fragment = new ProcessAudioRawData(); + break; + default: + fragment = new JoinChannelAudio(); + break; + } + getSupportFragmentManager().beginTransaction() + .replace(R.id.fragment_Layout, fragment) + .commit(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + this.finish(); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/MainActivity.java b/Android/APIExample/app/src/main/java/io/agora/api/example/MainActivity.java new file mode 100644 index 000000000..73062c4fa --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/MainActivity.java @@ -0,0 +1,66 @@ +package io.agora.api.example; + +import android.content.Intent; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuItem; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.constraintlayout.solver.GoalRow; +import androidx.navigation.ActionOnlyNavDirections; +import androidx.navigation.NavController; +import androidx.navigation.Navigation; +import androidx.navigation.ui.AppBarConfiguration; +import androidx.navigation.ui.NavigationUI; + +import io.agora.api.component.Constant; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.model.ExampleBean; + +/** + * @author cjw + */ +public class MainActivity extends AppCompatActivity implements MainFragment.OnListFragmentInteractionListener { + private AppBarConfiguration appBarConfiguration; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment); + appBarConfiguration = new AppBarConfiguration.Builder(navController.getGraph()).build(); + NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration); + } + + @Override + public boolean onSupportNavigateUp() { + NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment); + return NavigationUI.navigateUp(navController, appBarConfiguration) + || super.onSupportNavigateUp(); + } + + @Override + public void onListFragmentInteraction(Example item) { + ExampleBean exampleBean = new ExampleBean(item.index(), item.group(), item.name(), item.actionId(), item.tipsId()); + Bundle bundle = new Bundle(); + bundle.putParcelable(Constant.DATA, exampleBean); + Navigation.findNavController(this, R.id.nav_host_fragment) + .navigate(R.id.action_mainFragment_to_Ready, bundle); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_main_activity, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == R.id.setting) { + startActivity(new Intent(this, SettingActivity.class)); + } + return false; + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/MainApplication.java b/Android/APIExample/app/src/main/java/io/agora/api/example/MainApplication.java new file mode 100644 index 000000000..877dc71e4 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/MainApplication.java @@ -0,0 +1,51 @@ +package io.agora.api.example; + +import android.app.Application; +import android.content.Context; + +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.Set; + +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.model.Examples; +import io.agora.api.example.common.model.GlobalSettings; +import io.agora.api.example.utils.ClassUtils; + +public class MainApplication extends Application { + + private GlobalSettings globalSettings; + + @Override + public void onCreate() { + super.onCreate(); + initExamples(); + } + + private void initExamples() { + try { + Set packageName = ClassUtils.getFileNameByPackageName(this, "io.agora.api.example.examples"); + for (String name : packageName) { + Class aClass = Class.forName(name); + Annotation[] declaredAnnotations = aClass.getAnnotations(); + for (Annotation annotation : declaredAnnotations) { + if (annotation instanceof Example) { + Example example = (Example) annotation; + Examples.addItem(example); + } + } + } + Examples.sortItem(); + } + catch (Exception e) { + e.printStackTrace(); + } + } + + public GlobalSettings getGlobalSettings() { + if(globalSettings == null){ + globalSettings = new GlobalSettings(); + } + return globalSettings; + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/MainFragment.java b/Android/APIExample/app/src/main/java/io/agora/api/example/MainFragment.java new file mode 100644 index 000000000..e1ae6e5e1 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/MainFragment.java @@ -0,0 +1,118 @@ +package io.agora.api.example; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; +import java.util.Map; + +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.adapter.ExampleSection; +import io.agora.api.example.common.model.Examples; +import io.github.luizgrp.sectionedrecyclerviewadapter.SectionedRecyclerViewAdapter; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.api.example.common.model.Examples.BASIC; + +/** + * A fragment representing a list of Items. + *

+ * Activities containing this fragment MUST implement the {@link OnListFragmentInteractionListener} + * interface. + */ +public class MainFragment extends Fragment { + // TODO: Customize parameter argument names + private static final String ARG_COLUMN_COUNT = "column-count"; + // TODO: Customize parameters + private int mColumnCount = 1; + private OnListFragmentInteractionListener mListener; + + /** + * Mandatory empty constructor for the fragment manager to instantiate the + * fragment (e.g. upon screen orientation changes). + */ + public MainFragment() { + } + + // TODO: Customize parameter initialization + @SuppressWarnings("unused") + public static MainFragment newInstance(int columnCount) { + MainFragment fragment = new MainFragment(); + Bundle args = new Bundle(); + args.putInt(ARG_COLUMN_COUNT, columnCount); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (getArguments() != null) { + mColumnCount = getArguments().getInt(ARG_COLUMN_COUNT); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_main, container, false); + + // Set the adapter + if (view instanceof RecyclerView) { + Context context = view.getContext(); + RecyclerView recyclerView = (RecyclerView) view; + if (mColumnCount <= 1) { + recyclerView.setLayoutManager(new LinearLayoutManager(context)); + } else { + recyclerView.setLayoutManager(new GridLayoutManager(context, mColumnCount)); + } + SectionedRecyclerViewAdapter sectionedAdapter = new SectionedRecyclerViewAdapter(); + sectionedAdapter.addSection(new ExampleSection(BASIC, Examples.ITEM_MAP.get(BASIC), mListener)); + sectionedAdapter.addSection(new ExampleSection(ADVANCED, Examples.ITEM_MAP.get(ADVANCED), mListener)); + recyclerView.setAdapter(sectionedAdapter); + } + return view; + } + + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof OnListFragmentInteractionListener) { + mListener = (OnListFragmentInteractionListener) context; + } else { + throw new RuntimeException(context.toString() + + " must implement OnListFragmentInteractionListener"); + } + } + + @Override + public void onDetach() { + super.onDetach(); + mListener = null; + } + + /** + * This interface must be implemented by activities that contain this + * fragment to allow an interaction in this fragment to be communicated + * to the activity and potentially other fragments contained in that + * activity. + *

+ * See the Android Training lesson Communicating with Other Fragments for more information. + */ + public interface OnListFragmentInteractionListener { + // TODO: Update argument type and name + void onListFragmentInteraction(Example item); + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/ReadyFragment.java b/Android/APIExample/app/src/main/java/io/agora/api/example/ReadyFragment.java new file mode 100644 index 000000000..f3224dfa0 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/ReadyFragment.java @@ -0,0 +1,75 @@ +package io.agora.api.example; + +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.AppCompatTextView; + +import io.agora.api.component.Constant; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.common.model.ExampleBean; + +/** + * @author cjw + */ +public class ReadyFragment extends BaseFragment { + private static final String TAG = ReadyFragment.class.getSimpleName(); + + private AppCompatTextView tips; + private ExampleBean exampleBean; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + exampleBean = getArguments().getParcelable(Constant.DATA); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_ready_layout, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + if(actionBar != null){ + actionBar.setTitle(exampleBean.getName()); + actionBar.setHomeButtonEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(true); + } + + tips = view.findViewById(R.id.tips); + tips.setText(getString(exampleBean.getTipsId())); + view.findViewById(R.id.next).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + handler.postDelayed(new Runnable() { + @Override + public void run() { + getActivity().onBackPressed(); + } + }, 300); + ExampleActivity.instance(getActivity(), exampleBean); + } + }); + } + + @Override + public void onPrepareOptionsMenu(@NonNull Menu menu) { + super.onPrepareOptionsMenu(menu); + menu.setGroupVisible(R.id.main_setting_group, false); + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/SettingActivity.java b/Android/APIExample/app/src/main/java/io/agora/api/example/SettingActivity.java new file mode 100644 index 000000000..14f4c6281 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/SettingActivity.java @@ -0,0 +1,124 @@ +package io.agora.api.example; + +import android.os.Bundle; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.AppCompatSpinner; +import androidx.appcompat.widget.AppCompatTextView; + +import io.agora.api.example.common.model.GlobalSettings; +import io.agora.rtc.RtcEngine; + +/** + * @author cjw + */ +public class SettingActivity extends AppCompatActivity implements AdapterView.OnItemSelectedListener { + private static final String TAG = SettingActivity.class.getSimpleName(); + + private AppCompatTextView sdkVersion; + private AppCompatSpinner orientationSpinner, fpsSpinner, dimensionSpinner; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_setting_layout); + ActionBar actionBar = getSupportActionBar(); + if(actionBar != null) + { + actionBar.setHomeButtonEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(true); + } + sdkVersion = findViewById(R.id.sdkVersion); + sdkVersion.setText(String.format(getString(R.string.sdkversion1), RtcEngine.getSdkVersion())); + orientationSpinner = findViewById(R.id.orientation_spinner); + fpsSpinner = findViewById(R.id.frame_rate_spinner); + dimensionSpinner = findViewById(R.id.dimension_spinner); + String[] mItems = getResources().getStringArray(R.array.orientations); + String[] labels = new String[mItems.length]; + for(int i = 0;i arrayAdapter =new ArrayAdapter(this,android.R.layout.simple_spinner_dropdown_item, labels); + orientationSpinner.setAdapter(arrayAdapter); + orientationSpinner.setOnItemSelectedListener(this); + fpsSpinner.setOnItemSelectedListener(this); + dimensionSpinner.setOnItemSelectedListener(this); + fetchGlobalSettings(); + } + + private void fetchGlobalSettings(){ + String[] mItems = getResources().getStringArray(R.array.orientations); + String selectedItem = ((MainApplication) getApplication()).getGlobalSettings().getVideoEncodingOrientation(); + int i = 0; + if(selectedItem!=null){ + for(String item : mItems){ + if(selectedItem.equals(item)){ + break; + } + i++; + } + } + orientationSpinner.setSelection(i); + mItems = getResources().getStringArray(R.array.fps); + selectedItem = ((MainApplication) getApplication()).getGlobalSettings().getVideoEncodingFrameRate(); + i = 0; + if(selectedItem!=null){ + for(String item : mItems){ + if(selectedItem.equals(item)){ + break; + } + i++; + } + } + fpsSpinner.setSelection(i); + mItems = getResources().getStringArray(R.array.dimensions); + selectedItem = ((MainApplication) getApplication()).getGlobalSettings().getVideoEncodingDimension(); + i = 0; + if(selectedItem!=null){ + for(String item : mItems){ + if(selectedItem.equals(item)){ + break; + } + i++; + } + } + dimensionSpinner.setSelection(i); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + this.finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onItemSelected(AdapterView adapterView, View view, int i, long l) { + if(adapterView.getId() == R.id.orientation_spinner){ + GlobalSettings globalSettings = ((MainApplication)getApplication()).getGlobalSettings(); + globalSettings.setVideoEncodingOrientation(getResources().getStringArray(R.array.orientations)[i]); + } + else if(adapterView.getId() == R.id.frame_rate_spinner){ + GlobalSettings globalSettings = ((MainApplication)getApplication()).getGlobalSettings(); + globalSettings.setVideoEncodingFrameRate(getResources().getStringArray(R.array.fps)[i]); + } + else if(adapterView.getId() == R.id.dimension_spinner){ + GlobalSettings globalSettings = ((MainApplication)getApplication()).getGlobalSettings(); + globalSettings.setVideoEncodingDimension(getResources().getStringArray(R.array.dimensions)[i]); + } + } + + @Override + public void onNothingSelected(AdapterView adapterView) { + + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/annotation/Example.java b/Android/APIExample/app/src/main/java/io/agora/api/example/annotation/Example.java new file mode 100644 index 000000000..bea15b542 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/annotation/Example.java @@ -0,0 +1,40 @@ +package io.agora.api.example.annotation; + +import android.os.Parcelable; + +import java.io.Serializable; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Example { + /** + * @return example index + */ + int index(); + /** + * @return group name + */ + String group() default ""; + + /** + * @return example name + */ + int name(); + + /** + * @return action ID + */ + int actionId(); + + /** + * @return tips ID + * */ + int tipsId(); +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/common/BaseFragment.java b/Android/APIExample/app/src/main/java/io/agora/api/example/common/BaseFragment.java new file mode 100644 index 000000000..60233b788 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/common/BaseFragment.java @@ -0,0 +1,49 @@ +package io.agora.api.example.common; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; + +public class BaseFragment extends Fragment +{ + protected Handler handler; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + handler = new Handler(Looper.getMainLooper()); + } + + protected void showAlert(String message) + { + Context context = getContext(); + if (context == null) { + return; + } + + new AlertDialog.Builder(context).setTitle("Tips").setMessage(message) + .setPositiveButton("OK", (dialog, which) -> dialog.dismiss()) + .show(); + } + + protected final void showLongToast(final String msg) + { + handler.post(new Runnable() + { + @Override + public void run() + { + if (BaseFragment.this == null || getContext() == null) + {return;} + Toast.makeText(getContext().getApplicationContext(), msg, Toast.LENGTH_LONG).show(); + } + }); + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/common/adapter/ExampleSection.java b/Android/APIExample/app/src/main/java/io/agora/api/example/common/adapter/ExampleSection.java new file mode 100644 index 000000000..31505545d --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/common/adapter/ExampleSection.java @@ -0,0 +1,79 @@ +package io.agora.api.example.common.adapter; + +import android.view.View; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; + +import io.agora.api.example.MainFragment; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.github.luizgrp.sectionedrecyclerviewadapter.Section; +import io.github.luizgrp.sectionedrecyclerviewadapter.SectionParameters; + +public class ExampleSection extends Section { + private final String mTitle; + private final List mValues; + private final MainFragment.OnListFragmentInteractionListener mListener; + + public ExampleSection(String title, List items, MainFragment.OnListFragmentInteractionListener listener) { + super(SectionParameters.builder().headerResourceId(R.layout.layout_main_list_section).itemResourceId(R.layout.layout_main_list_item).build()); + mTitle = title; + mValues = items; + mListener = listener; + } + + @Override + public int getContentItemsTotal() { + return mValues.size(); + } + + @Override + public RecyclerView.ViewHolder getItemViewHolder(View view) { + return new ViewHolder(view); + } + + @Override + public void onBindItemViewHolder(RecyclerView.ViewHolder viewHolder, int position) { + if (viewHolder instanceof ViewHolder) { + ViewHolder holder = (ViewHolder) viewHolder; + holder.mItem = mValues.get(position); + holder.mNameView.setText(holder.mView.getContext().getString(holder.mItem.name())); + + holder.mView.setOnClickListener(v -> { + if (null != mListener) { + // Notify the active callbacks interface (the activity, if the + // fragment is attached to one) that an item has been selected. + mListener.onListFragmentInteraction(holder.mItem); + } + }); + } + } + + @Override + public RecyclerView.ViewHolder getHeaderViewHolder(View view) { + return new ViewHolder(view); + } + + @Override + public void onBindHeaderViewHolder(RecyclerView.ViewHolder viewHolder) { + if (viewHolder instanceof ViewHolder) { + ViewHolder holder = (ViewHolder) viewHolder; + holder.mNameView.setText(mTitle); + } + } + + static class ViewHolder extends RecyclerView.ViewHolder { + final View mView; + final TextView mNameView; + Example mItem; + + ViewHolder(View view) { + super(view); + mView = view; + mNameView = view.findViewById(R.id.item_name); + } + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/common/model/ExampleBean.java b/Android/APIExample/app/src/main/java/io/agora/api/example/common/model/ExampleBean.java new file mode 100644 index 000000000..331ae96a7 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/common/model/ExampleBean.java @@ -0,0 +1,99 @@ +package io.agora.api.example.common.model; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * @author cjw + */ +public class ExampleBean implements Parcelable { + private int index; + private String group; + private int name; + private int actionId; + private int tipsId; + + public ExampleBean(int index, String group, int name, int actionId, int tipsId) { + this.index = index; + this.group = group; + this.name = name; + this.actionId = actionId; + this.tipsId = tipsId; + } + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public int getName() { + return name; + } + + public void setName(int name) { + this.name = name; + } + + public int getActionId() { + return actionId; + } + + public void setActionId(int actionId) { + this.actionId = actionId; + } + + public int getTipsId() { + return tipsId; + } + + public void setTipsId(int tipsId) { + this.tipsId = tipsId; + } + + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(this.group); + dest.writeInt(this.name); + dest.writeInt(this.actionId); + dest.writeInt(this.tipsId); + } + + public ExampleBean() { + } + + protected ExampleBean(Parcel in) { + this.group = in.readString(); + this.name = in.readInt(); + this.actionId = in.readInt(); + this.tipsId = in.readInt(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public ExampleBean createFromParcel(Parcel source) { + return new ExampleBean(source); + } + + @Override + public ExampleBean[] newArray(int size) { + return new ExampleBean[size]; + } + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/common/model/Examples.java b/Android/APIExample/app/src/main/java/io/agora/api/example/common/model/Examples.java new file mode 100644 index 000000000..80ec5092b --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/common/model/Examples.java @@ -0,0 +1,41 @@ +package io.agora.api.example.common.model; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.agora.api.example.annotation.Example; + +public class Examples { + public static final String BASIC = "BASIC"; + public static final String ADVANCED = "ADVANCED"; + + public static final Map> ITEM_MAP = new HashMap<>(); + + public static void addItem(@NonNull Example item) { + String group = item.group(); + List list = ITEM_MAP.get(group); + if (list == null) { + list = new ArrayList<>(); + ITEM_MAP.put(group, list); + } + list.add(item); + } + + public static void sortItem() { + for (Map.Entry> entry : ITEM_MAP.entrySet()) { + List exampleList = ITEM_MAP.get(entry.getKey()); + Collections.sort(exampleList, new Comparator() { + @Override + public int compare(Example o1, Example o2) { + return o1.index() - o2.index(); + } + }); + } + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/common/model/GlobalSettings.java b/Android/APIExample/app/src/main/java/io/agora/api/example/common/model/GlobalSettings.java new file mode 100644 index 000000000..40a6496fc --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/common/model/GlobalSettings.java @@ -0,0 +1,66 @@ +package io.agora.api.example.common.model; + +import android.util.Log; + +import java.lang.reflect.Field; + +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static io.agora.rtc.video.VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15; +import static io.agora.rtc.video.VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; +import static io.agora.rtc.video.VideoEncoderConfiguration.VD_640x360; + +public class GlobalSettings { + private String videoEncodingDimension; + private String videoEncodingFrameRate; + private String videoEncodingOrientation; + + public String getVideoEncodingDimension() { + if(videoEncodingDimension == null) + return "VD_640x360"; + else + return videoEncodingDimension; + } + + public VideoEncoderConfiguration.VideoDimensions getVideoEncodingDimensionObject() { + if(videoEncodingDimension == null) + return VD_640x360; + VideoEncoderConfiguration.VideoDimensions value = VD_640x360; + try { + Field tmp = VideoEncoderConfiguration.class.getDeclaredField(videoEncodingDimension); + tmp.setAccessible(true); + value = (VideoEncoderConfiguration.VideoDimensions) tmp.get(null); + } catch (NoSuchFieldException e) { + Log.e("Field", "Can not find field " + videoEncodingDimension); + } catch (IllegalAccessException e) { + Log.e("Field", "Could not access field " + videoEncodingDimension); + } + return value; + } + + public void setVideoEncodingDimension(String videoEncodingDimension) { + this.videoEncodingDimension = videoEncodingDimension; + } + + public String getVideoEncodingFrameRate() { + if(videoEncodingFrameRate == null) + return FRAME_RATE_FPS_15.name(); + else + return videoEncodingFrameRate; + } + + public void setVideoEncodingFrameRate(String videoEncodingFrameRate) { + this.videoEncodingFrameRate = videoEncodingFrameRate; + } + + public String getVideoEncodingOrientation() { + if(videoEncodingOrientation == null) + return ORIENTATION_MODE_ADAPTIVE.name(); + else + return videoEncodingOrientation; + } + + public void setVideoEncodingOrientation(String videoEncodingOrientation) { + this.videoEncodingOrientation = videoEncodingOrientation; + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/common/model/Peer.java b/Android/APIExample/app/src/main/java/io/agora/api/example/common/model/Peer.java new file mode 100644 index 000000000..676f7c7ec --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/common/model/Peer.java @@ -0,0 +1,16 @@ +package io.agora.api.example.common.model; + +import java.nio.ByteBuffer; + +/** + * Created by wyylling@gmail.com on 03/01/2018. + */ + +public class Peer { + public int uid; + public ByteBuffer data; + public int width; + public int height; + public int rotation; + public long ts; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/common/model/StatisticsInfo.java b/Android/APIExample/app/src/main/java/io/agora/api/example/common/model/StatisticsInfo.java new file mode 100644 index 000000000..dd27ae8c9 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/common/model/StatisticsInfo.java @@ -0,0 +1,168 @@ +package io.agora.api.example.common.model; + +import io.agora.rtc.IRtcEngineEventHandler.*; + +public class StatisticsInfo { + private LocalVideoStats localVideoStats; + private LocalAudioStats localAudioStats; + private RemoteVideoStats remoteVideoStats; + private RemoteAudioStats remoteAudioStats; + private RtcStats rtcStats; + private int quality; + private LastmileProbeResult lastMileProbeResult; + + public void setLocalVideoStats(LocalVideoStats localVideoStats) { + this.localVideoStats = localVideoStats; + } + + public void setLocalAudioStats(LocalAudioStats localAudioStats) { + this.localAudioStats = localAudioStats; + } + + public void setRemoteVideoStats(RemoteVideoStats remoteVideoStats) { + this.remoteVideoStats = remoteVideoStats; + } + + public void setRemoteAudioStats(RemoteAudioStats remoteAudioStats) { + this.remoteAudioStats = remoteAudioStats; + } + + public void setRtcStats(RtcStats rtcStats) { + this.rtcStats = rtcStats; + } + + public String getLocalVideoStats() { + StringBuilder builder = new StringBuilder(); + return builder + .append(localVideoStats.encodedFrameWidth) + .append("×") + .append(localVideoStats.encodedFrameHeight) + .append(",") + .append(localVideoStats.encoderOutputFrameRate) + .append("fps") + .append("\n") + .append("LM Delay: ") + .append(rtcStats.lastmileDelay) + .append("ms") + .append("\n") + .append("VSend: ") + .append(localVideoStats.sentBitrate) + .append("kbps") + .append("\n") + .append("ASend: ") + .append(localAudioStats.sentBitrate) + .append("kbps") + .append("\n") + .append("CPU: ") + .append(rtcStats.cpuAppUsage) + .append("%/") + .append(rtcStats.cpuTotalUsage) + .append("%/") + .append("\n") + .append("VSend Loss: ") + .append(localVideoStats.txPacketLossRate) + .append("%") + .append("\n") + .append("ASend Loss: ") + .append(localAudioStats.txPacketLossRate) + .append("%") + .toString(); + } + + public String getRemoteVideoStats() { + StringBuilder builder = new StringBuilder(); + return builder + .append(remoteVideoStats.width) + .append("×") + .append(remoteVideoStats.height) + .append(",") + .append(remoteVideoStats.rendererOutputFrameRate) + .append("fps") + .append("\n") + .append("VRecv: ") + .append(remoteVideoStats.receivedBitrate) + .append("kbps") + .append("\n") + .append("ARecv: ") + .append(remoteAudioStats.receivedBitrate) + .append("kbps") + .append("\n") + .append("VLoss: ") + .append(remoteVideoStats.packetLossRate) + .append("%") + .append("\n") + .append("ALoss: ") + .append(remoteAudioStats.audioLossRate) + .append("%") + .append("\n") + .append("AQuality: ") + .append(remoteAudioStats.quality) + .toString(); + } + + public void setLastMileQuality(int quality) { + this.quality = quality; + } + + public String getLastMileQuality(){ + switch (quality){ + case 1: + return "EXCELLENT"; + case 2: + return "GOOD"; + case 3: + return "POOR"; + case 4: + return "BAD"; + case 5: + return "VERY BAD"; + case 6: + return "DOWN"; + case 7: + return "UNSUPPORTED"; + case 8: + return "DETECTING"; + default: + return "UNKNOWN"; + } + } + + public String getLastMileResult() { + if(lastMileProbeResult == null) + return null; + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("Rtt: ") + .append(lastMileProbeResult.rtt) + .append("ms") + .append("\n") + .append("DownlinkAvailableBandwidth: ") + .append(lastMileProbeResult.downlinkReport.availableBandwidth) + .append("Kbps") + .append("\n") + .append("DownlinkJitter: ") + .append(lastMileProbeResult.downlinkReport.jitter) + .append("ms") + .append("\n") + .append("DownlinkLoss: ") + .append(lastMileProbeResult.downlinkReport.packetLossRate) + .append("%") + .append("\n") + .append("UplinkAvailableBandwidth: ") + .append(lastMileProbeResult.uplinkReport.availableBandwidth) + .append("Kbps") + .append("\n") + .append("UplinkJitter: ") + .append(lastMileProbeResult.uplinkReport.jitter) + .append("ms") + .append("\n") + .append("UplinkLoss: ") + .append(lastMileProbeResult.uplinkReport.packetLossRate) + .append("%"); + return stringBuilder.toString(); + } + + public void setLastMileProbeResult(LastmileProbeResult lastmileProbeResult) { + this.lastMileProbeResult = lastmileProbeResult; + } + +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ARCore.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ARCore.java new file mode 100644 index 000000000..737f52d6e --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ARCore.java @@ -0,0 +1,767 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.graphics.Bitmap; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.text.TextUtils; +import android.util.Log; +import android.view.GestureDetector; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.PixelCopy; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import com.google.ar.core.Anchor; +import com.google.ar.core.ArCoreApk; +import com.google.ar.core.Camera; +import com.google.ar.core.Config; +import com.google.ar.core.Frame; +import com.google.ar.core.HitResult; +import com.google.ar.core.Plane; +import com.google.ar.core.Point; +import com.google.ar.core.PointCloud; +import com.google.ar.core.Session; +import com.google.ar.core.Trackable; +import com.google.ar.core.TrackingState; +import com.google.ar.core.exceptions.CameraNotAvailableException; +import com.google.ar.core.exceptions.UnavailableApkTooOldException; +import com.google.ar.core.exceptions.UnavailableArcoreNotInstalledException; +import com.google.ar.core.exceptions.UnavailableSdkTooOldException; +import com.google.ar.core.exceptions.UnavailableUserDeclinedInstallationException; +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.examples.advanced.customvideo.AgoraVideoRender; +import io.agora.api.example.examples.advanced.customvideo.AgoraVideoSource; +import io.agora.api.example.examples.advanced.customvideo.BackgroundRenderer; +import io.agora.api.example.examples.advanced.customvideo.DisplayRotationHelper; +import io.agora.api.example.examples.advanced.customvideo.ObjectRenderer; +import io.agora.api.example.examples.advanced.customvideo.PeerRenderer; +import io.agora.api.example.examples.advanced.customvideo.PlaneRenderer; +import io.agora.api.example.examples.advanced.customvideo.PointCloudRenderer; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.mediaio.MediaIO; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; +import static io.agora.rtc.video.VideoEncoderConfiguration.STANDARD_BITRATE; + +/**This demo demonstrates how to make a one-to-one video call*/ +@Example( + index = 24, + group = ADVANCED, + name = R.string.item_arcore, + actionId = R.id.action_mainFragment_arcore, + tipsId = R.string.arcore +) +public class ARCore extends BaseFragment implements View.OnClickListener, GLSurfaceView.Renderer +{ + private static final String TAG = ARCore.class.getSimpleName(); + + private Button join; + private EditText et_channel; + private RtcEngine engine; + private int myUid; + private boolean joined = false; + private AgoraVideoSource mSource; + private AgoraVideoRender mRender; + private float mScaleFactor = 1.0f; + + // Rendering. The Renderers are created here, and initialized when the GL surface is created. + private GLSurfaceView mSurfaceView; + private GestureDetector mGestureDetector; + private Session mSession; + private ByteBuffer mSendBuffer; + + private boolean installRequested; + + // Tap handling and UI. + private final ArrayBlockingQueue queuedSingleTaps = new ArrayBlockingQueue<>(16); + private final ArrayList anchors = new ArrayList<>(); + private DisplayRotationHelper mDisplayRotationHelper; + private PeerRenderer mPeerObject = new PeerRenderer(); + + private final BackgroundRenderer mBackgroundRenderer = new BackgroundRenderer(); + private final ObjectRenderer mVirtualObject = new ObjectRenderer(); + private final ObjectRenderer mVirtualObjectShadow = new ObjectRenderer(); + private final PlaneRenderer mPlaneRenderer = new PlaneRenderer(); + private final PointCloudRenderer mPointCloud = new PointCloudRenderer(); + + // Temporary matrix allocated here to reduce number of allocations for each frame. + private final float[] mAnchorMatrix = new float[16]; + + private List mRemoteRenders = new ArrayList<>(20); + private Handler mSenderHandler; + + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_arcore, container, false); + return view; + } + + @RequiresApi(api = Build.VERSION_CODES.M) + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + try + { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); + } + catch (Exception e) + { + e.printStackTrace(); + getActivity().onBackPressed(); + } + join = view.findViewById(R.id.btn_join); + et_channel = view.findViewById(R.id.et_channel); + et_channel.setText("arcoreDemo"); + joinChannel("arcoreDemo"); + view.findViewById(R.id.btn_join).setOnClickListener(this); + mSurfaceView = view.findViewById(R.id.fl_local); + mDisplayRotationHelper = new DisplayRotationHelper(getContext()); + + mSurfaceView.setOnTouchListener( + new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + return mGestureDetector.onTouchEvent(event); + } + }); + // Set up tap listener. + mGestureDetector = + new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onSingleTapUp(MotionEvent e) { + onSingleTap(e); + return true; + } + + @Override + public boolean onDown(MotionEvent e) { + return true; + } + }); + // Set up renderer. + mSurfaceView.setPreserveEGLContextOnPause(true); + mSurfaceView.setEGLContextClientVersion(2); + mSurfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0); // Alpha used for plane blending. + mSurfaceView.setRenderer(this); + mSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); + } + + private void onSingleTap(MotionEvent e) { + // Queue tap if there is space. Tap is lost if queue is full. + queuedSingleTaps.offer(e); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) + { + super.onActivityCreated(savedInstanceState); + } + + @Override + public void onDestroy() + { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if(engine != null) + { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + mSendBuffer = null; + for (int i = 0; i < mRemoteRenders.size(); ++i) { + AgoraVideoRender render = mRemoteRenders.get(i); + //mRtcEngine.setRemoteVideoRenderer(render.getPeer().uid, null); + } + mRemoteRenders.clear(); + mSenderHandler.getLooper().quit(); + } + + @Override + public void onClick(View v) + { + if (v.getId() == R.id.btn_join) + { + if (!joined) + { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) + { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE, + Permission.Group.CAMERA + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } + else + { + joined = false; + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + join.setText(getString(R.string.join)); + } + } + } + + private void joinChannel(String channelId) + { + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + + // Set audio route to microPhone + engine.setDefaultAudioRoutetoSpeakerphone(false); + + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + // Enable video module + engine.enableVideo(); + // Setup video encoding configs + + engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + ((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(), + VideoEncoderConfiguration.FRAME_RATE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate()), + STANDARD_BITRATE, + VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) + )); + + mSource = new AgoraVideoSource(); + mRender = new AgoraVideoRender(0, true); + engine.setVideoSource(mSource); + engine.setLocalVideoRenderer(mRender); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) + { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0,option); + if (res != 0) + { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + + HandlerThread thread = new HandlerThread("ArSendThread"); + thread.start(); + mSenderHandler = new Handler(thread.getLooper()); + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() + { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) + { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) + { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) + { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @RequiresApi(api = Build.VERSION_CODES.M) + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) + { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() + { + @Override + public void run() + { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + } + }); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Since v2.9.0. + * Occurs when the remote video state changes. + * PS: This callback does not work properly when the number of users (in the Communication + * profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the remote user whose video state changes. + * @param state State of the remote video: + * REMOTE_VIDEO_STATE_STOPPED(0): The remote video is in the default state, probably due + * to REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3), REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5), + * or REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7). + * REMOTE_VIDEO_STATE_STARTING(1): The first remote video packet is received. + * REMOTE_VIDEO_STATE_DECODING(2): The remote video stream is decoded and plays normally, + * probably due to REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY (2), + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4), REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6), + * or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9). + * REMOTE_VIDEO_STATE_FROZEN(3): The remote video is frozen, probably due to + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1) or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8). + * REMOTE_VIDEO_STATE_FAILED(4): The remote video fails to start, probably due to + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0). + * @param reason The reason of the remote video state change: + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0): Internal reasons. + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3): The local user stops receiving the remote + * video stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote + * video stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5): The remote user stops sending the video + * stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the video + * stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8): The remote media stream falls back to the + * audio-only stream due to poor network conditions. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9): The remote media stream switches + * back to the video stream after the network conditions improve. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method until + * the SDK triggers this callback.*/ + @Override + public void onRemoteVideoStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteVideoStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteVideoStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) + { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) + { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + } + }; + + + @RequiresApi(api = Build.VERSION_CODES.M) + @Override + public void onResume() { + super.onResume(); + if (mSession == null) { + Exception exception = null; + String message = null; + try { + switch (ArCoreApk.getInstance().requestInstall(getActivity(), !installRequested)) { + case INSTALL_REQUESTED: + installRequested = true; + return; + case INSTALLED: + break; + } + + mSession = new Session(getContext()); + } catch (UnavailableArcoreNotInstalledException + | UnavailableUserDeclinedInstallationException e) { + message = "Please install ARCore"; + exception = e; + } catch (UnavailableApkTooOldException e) { + message = "Please update ARCore"; + exception = e; + } catch (UnavailableSdkTooOldException e) { + message = "Please update this app"; + exception = e; + } catch (Exception e) { + message = "This device does not support AR"; + exception = e; + } + + if (message != null) { + showLongToast(message); + Log.e(TAG, "Exception creating session", exception); + return; + } + + // Create default config and check if supported. + Config config = new Config(mSession); + if (!mSession.isSupported(config)) { + showLongToast("This device does not support AR"); + } + mSession.configure(config); + } + + // Note that order matters - see the note in onPause(), the reverse applies here. + try { + mSession.resume(); + } catch (CameraNotAvailableException e) { + Log.e(TAG, e.getMessage()); + } + mSurfaceView.onResume(); + mDisplayRotationHelper.onResume(); + } + + + @RequiresApi(api = Build.VERSION_CODES.N) + private void sendARViewMessage() { + final Bitmap outBitmap = Bitmap.createBitmap(mSurfaceView.getWidth(), mSurfaceView.getHeight(), Bitmap.Config.ARGB_8888); + PixelCopy.request(mSurfaceView, outBitmap, new PixelCopy.OnPixelCopyFinishedListener() { + @Override + public void onPixelCopyFinished(int copyResult) { + if (copyResult == PixelCopy.SUCCESS) { + sendARView(outBitmap); + } else { + Toast.makeText(getContext(), "Pixel Copy Failed", Toast.LENGTH_SHORT); + } + } + }, mSenderHandler); + } + + + private void sendARView(Bitmap bitmap) { + if (bitmap == null) return; + + if (mSource.getConsumer() == null) return; + + //Bitmap bitmap = source.copy(Bitmap.Config.ARGB_8888,true); + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + + int size = bitmap.getRowBytes() * bitmap.getHeight(); + ByteBuffer byteBuffer = ByteBuffer.allocate(size); + bitmap.copyPixelsToBuffer(byteBuffer); + byte[] data = byteBuffer.array(); + + mSource.getConsumer().consumeByteArrayFrame(data, MediaIO.PixelFormat.RGBA.intValue(), width, height, 0, System.currentTimeMillis()); + } + + @Override + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + GLES20.glClearColor(0.1f, 0.1f, 0.1f, 1.0f); + + // Create the texture and pass it to ARCore session to be filled during update(). + mBackgroundRenderer.createOnGlThread(getContext()); + if (mSession != null) { + mSession.setCameraTextureName(mBackgroundRenderer.getTextureId()); + } + + // Prepare the other rendering objects. + try { + mVirtualObject.createOnGlThread(getContext(), "andy.obj", "andy.png"); + mVirtualObject.setMaterialProperties(0.0f, 3.5f, 1.0f, 6.0f); + + mVirtualObjectShadow.createOnGlThread(getContext(), + "andy_shadow.obj", "andy_shadow.png"); + mVirtualObjectShadow.setBlendMode(ObjectRenderer.BlendMode.Shadow); + mVirtualObjectShadow.setMaterialProperties(1.0f, 0.0f, 0.0f, 1.0f); + } catch (IOException e) { + Log.e(TAG, "Failed to read obj file"); + } + try { + mPlaneRenderer.createOnGlThread(getContext(), "trigrid.png"); + } catch (IOException e) { + Log.e(TAG, "Failed to read plane texture"); + } + mPointCloud.createOnGlThread(getContext()); + + try { + mPeerObject.createOnGlThread(getContext()); + } catch (IOException ex) { + Log.e(TAG, ex.getMessage()); + } + } + + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + mDisplayRotationHelper.onSurfaceChanged(width, height); + GLES20.glViewport(0, 0, width, height); + } + + @Override + public void onDrawFrame(GL10 gl10) { + // Clear screen to notify driver it should not load any pixels from previous frame. + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT); + + if (mSession == null) { + return; + } + // Notify ARCore session that the view size changed so that the perspective matrix and + // the video background can be properly adjusted. + mDisplayRotationHelper.updateSessionIfNeeded(mSession); + + try { + // Obtain the current frame from ARSession. When the configuration is set to + // UpdateMode.BLOCKING (it is by default), this will throttle the rendering to the + // camera framerate. + Frame frame = mSession.update(); + Camera camera = frame.getCamera(); + + // Handle taps. Handling only one tap per frame, as taps are usually low frequency + // compared to frame rate. + MotionEvent tap = queuedSingleTaps.poll(); + if (tap != null && camera.getTrackingState() == TrackingState.TRACKING) { + for (HitResult hit : frame.hitTest(tap)) { + // Check if any plane was hit, and if it was hit inside the plane polygon + Trackable trackable = hit.getTrackable(); + // Creates an anchor if a plane or an oriented point was hit. + if ((trackable instanceof Plane && ((Plane) trackable).isPoseInPolygon(hit.getHitPose())) + || (trackable instanceof Point + && ((Point) trackable).getOrientationMode() + == Point.OrientationMode.ESTIMATED_SURFACE_NORMAL)) { + // Hits are sorted by depth. Consider only closest hit on a plane or oriented point. + // Cap the number of objects created. This avoids overloading both the + // rendering system and ARCore. + if (anchors.size() >= 20) { + anchors.get(0).detach(); + anchors.remove(0); + } + // Adding an Anchor tells ARCore that it should track this position in + // space. This anchor is created on the Plane to place the 3D model + // in the correct position relative both to the world and to the plane. + anchors.add(hit.createAnchor()); + break; + } + } + } + + // Draw background. + mBackgroundRenderer.draw(frame); + + // If not tracking, don't draw 3d objects. + if (camera.getTrackingState() == TrackingState.PAUSED) { + return; + } + + // Get projection matrix. + float[] projmtx = new float[16]; + camera.getProjectionMatrix(projmtx, 0, 0.1f, 100.0f); + + // Get camera matrix and draw. + float[] viewmtx = new float[16]; + camera.getViewMatrix(viewmtx, 0); + + // Compute lighting from average intensity of the image. + final float lightIntensity = frame.getLightEstimate().getPixelIntensity(); + + // Visualize planes. + mPlaneRenderer.drawPlanes( + mSession.getAllTrackables(Plane.class), camera.getDisplayOrientedPose(), projmtx); + + // Visualize anchors created by touch. + float scaleFactor = 1.0f; + + int i = 0; + for (Anchor anchor : anchors) { + if (anchor.getTrackingState() != TrackingState.TRACKING) { + continue; + } + // Get the current pose of an Anchor in world space. The Anchor pose is updated + // during calls to session.update() as ARCore refines its estimate of the world. + anchor.getPose().toMatrix(mAnchorMatrix, 0); + + // Update and draw the model and its shadow. + mVirtualObject.updateModelMatrix(mAnchorMatrix, mScaleFactor); + mVirtualObjectShadow.updateModelMatrix(mAnchorMatrix, scaleFactor); + mVirtualObject.draw(viewmtx, projmtx, lightIntensity); + mVirtualObjectShadow.draw(viewmtx, projmtx, lightIntensity); + } + + sendARViewMessage(); + + } catch (Throwable t) { + // Avoid crashing the application due to unhandled exceptions. + Log.e(TAG, "Exception on the OpenGL thread", t); + } + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/AdjustVolume.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/AdjustVolume.java new file mode 100755 index 000000000..8d0b000a4 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/AdjustVolume.java @@ -0,0 +1,399 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.SeekBar; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.models.ChannelMediaOptions; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.api.example.common.model.Examples.BASIC; + +@Example( + index = 19, + group = ADVANCED, + name = R.string.item_adjustvolume, + actionId = R.id.action_mainFragment_to_AdjustVolume, + tipsId = R.string.adjustvolume +) +public class AdjustVolume extends BaseFragment implements View.OnClickListener { + private static final String TAG = AdjustVolume.class.getSimpleName(); + private EditText et_channel; + private Button mute, join, speaker; + private RtcEngine engine; + private int myUid; + private boolean joined = false; + private SeekBar record, playout, inear; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + handler = new Handler(); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_adjust_volume, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + et_channel = view.findViewById(R.id.et_channel); + view.findViewById(R.id.btn_join).setOnClickListener(this); + mute = view.findViewById(R.id.btn_mute); + mute.setOnClickListener(this); + speaker = view.findViewById(R.id.btn_speaker); + speaker.setOnClickListener(this); + record = view.findViewById(R.id.recordingVol); + playout = view.findViewById(R.id.playoutVol); + inear = view.findViewById(R.id.inEarMonitorVol); + record.setOnSeekBarChangeListener(seekBarChangeListener); + playout.setOnSeekBarChangeListener(seekBarChangeListener); + inear.setOnSeekBarChangeListener(seekBarChangeListener); + record.setEnabled(false); + playout.setEnabled(false); + inear.setEnabled(false); + } + + SeekBar.OnSeekBarChangeListener seekBarChangeListener = new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if(seekBar.getId() == record.getId()){ + engine.adjustRecordingSignalVolume(progress); + } + else if(seekBar.getId() == playout.getId()){ + engine.adjustPlaybackSignalVolume(progress); + } + else if(seekBar.getId() == inear.getId()){ + if(progress == 0){ + engine.enableInEarMonitoring(false); + } + else { + engine.enableInEarMonitoring(true); + engine.setInEarMonitoringVolume(progress); + } + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + + } + }; + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) { + return; + } + try { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + String appId = getString(R.string.agora_app_id); + engine = RtcEngine.create(getContext().getApplicationContext(), appId, iRtcEngineEventHandler); + } + catch (Exception e) { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if (engine != null) { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.btn_join) { + if (!joined) { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } else { + joined = false; + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + join.setText(getString(R.string.join)); + speaker.setText(getString(R.string.speaker)); + speaker.setEnabled(false); + mute.setText(getString(R.string.closemicrophone)); + mute.setEnabled(false); + record.setEnabled(false); + record.setProgress(0); + playout.setEnabled(false); + playout.setProgress(0); + inear.setEnabled(false); + inear.setProgress(0); + } + } else if (v.getId() == R.id.btn_mute) { + mute.setActivated(!mute.isActivated()); + mute.setText(getString(mute.isActivated() ? R.string.openmicrophone : R.string.closemicrophone)); + /**Turn off / on the microphone, stop / start local audio collection and push streaming.*/ + engine.muteLocalAudioStream(mute.isActivated()); + } else if (v.getId() == R.id.btn_speaker) { + speaker.setActivated(!speaker.isActivated()); + speaker.setText(getString(speaker.isActivated() ? R.string.earpiece : R.string.speaker)); + /**Turn off / on the speaker and change the audio playback route.*/ + engine.setEnableSpeakerphone(speaker.isActivated()); + } + } + + /** + * @param channelId Specify the channel name that you want to join. + * Users that input the same channel name join the same channel. + */ + private void joinChannel(String channelId) { + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + engine.enableAudioVolumeIndication(1000, 3, true); + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0,option); + if (res != 0) { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + + + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() { + @Override + public void run() { + speaker.setEnabled(true); + mute.setEnabled(true); + join.setEnabled(true); + join.setText(getString(R.string.leave)); + record.setEnabled(true); + record.setProgress(100); + playout.setEnabled(true); + playout.setProgress(100); + inear.setEnabled(true); + } + }); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + } + + @Override + public void onActiveSpeaker(int uid) { + super.onActiveSpeaker(uid); + Log.i(TAG, String.format("onActiveSpeaker:%d", uid)); + } + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ChannelEncryption.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ChannelEncryption.java new file mode 100644 index 000000000..72a0712f8 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ChannelEncryption.java @@ -0,0 +1,469 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.Spinner; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.internal.EncryptionConfig; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; +import static io.agora.rtc.video.VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15; +import static io.agora.rtc.video.VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; +import static io.agora.rtc.video.VideoEncoderConfiguration.STANDARD_BITRATE; +import static io.agora.rtc.video.VideoEncoderConfiguration.VD_640x360; + +/**This demo demonstrates how to make a one-to-one video call*/ +@Example( + index = 22, + group = ADVANCED, + name = R.string.item_channelencryption, + actionId = R.id.action_mainFragment_to_channel_encryption, + tipsId = R.string.channelencryption +) +public class ChannelEncryption extends BaseFragment implements View.OnClickListener +{ + private static final String TAG = ChannelEncryption.class.getSimpleName(); + + private FrameLayout fl_local, fl_remote; + private Button join; + private EditText et_channel, et_password; + private Spinner encry_mode; + private RtcEngine engine; + private int myUid; + private boolean joined = false; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_channel_encryption, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + et_channel = view.findViewById(R.id.et_channel); + et_password = view.findViewById(R.id.et_encry_pass); + view.findViewById(R.id.btn_join).setOnClickListener(this); + fl_local = view.findViewById(R.id.fl_local); + fl_remote = view.findViewById(R.id.fl_remote); + encry_mode = view.findViewById(R.id.encry_mode_spinner); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) + { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + try + { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); + } + catch (Exception e) + { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() + { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if(engine != null) + { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + } + + @Override + public void onClick(View v) + { + if (v.getId() == R.id.btn_join) + { + if (!joined) + { + // Creates an EncryptionConfig instance. + EncryptionConfig config = new EncryptionConfig(); + // Sets the encryption mode as AES_128_XTS. + config.encryptionMode = EncryptionConfig.EncryptionMode.valueOf(encry_mode.getSelectedItem().toString()); + // Sets the encryption key. + config.encryptionKey = et_password.getText().toString(); + // Enables the built-in encryption. + engine.enableEncryption(true, config); + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) + { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE, + Permission.Group.CAMERA + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } + else + { + joined = false; + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + join.setText(getString(R.string.join)); + et_password.setEnabled(true); + encry_mode.setEnabled(true); + } + } + } + + private void joinChannel(String channelId) + { + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + + // Create render view by RtcEngine + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + if(fl_local.getChildCount() > 0) + { + fl_local.removeAllViews(); + } + // Add to the local container + fl_local.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup local video to render your local camera preview + engine.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0)); + // Set audio route to microPhone + engine.setDefaultAudioRoutetoSpeakerphone(false); + + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + // Enable video module + engine.enableVideo(); + // Setup video encoding configs + engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + ((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(), + VideoEncoderConfiguration.FRAME_RATE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate()), + STANDARD_BITRATE, + VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) + )); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) + { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0,option); + if (res != 0) + { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() + { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) + { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) + { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) + { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) + { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() + { + @Override + public void run() + { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + et_password.setEnabled(false); + encry_mode.setEnabled(false); + } + }); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Since v2.9.0. + * Occurs when the remote video state changes. + * PS: This callback does not work properly when the number of users (in the Communication + * profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the remote user whose video state changes. + * @param state State of the remote video: + * REMOTE_VIDEO_STATE_STOPPED(0): The remote video is in the default state, probably due + * to REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3), REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5), + * or REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7). + * REMOTE_VIDEO_STATE_STARTING(1): The first remote video packet is received. + * REMOTE_VIDEO_STATE_DECODING(2): The remote video stream is decoded and plays normally, + * probably due to REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY (2), + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4), REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6), + * or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9). + * REMOTE_VIDEO_STATE_FROZEN(3): The remote video is frozen, probably due to + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1) or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8). + * REMOTE_VIDEO_STATE_FAILED(4): The remote video fails to start, probably due to + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0). + * @param reason The reason of the remote video state change: + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0): Internal reasons. + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3): The local user stops receiving the remote + * video stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote + * video stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5): The remote user stops sending the video + * stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the video + * stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8): The remote media stream falls back to the + * audio-only stream due to poor network conditions. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9): The remote media stream switches + * back to the video stream after the network conditions improve. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method until + * the SDK triggers this callback.*/ + @Override + public void onRemoteVideoStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteVideoStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteVideoStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) + { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + handler.post(() -> + { + /**Display remote video stream*/ + SurfaceView surfaceView = null; + if (fl_remote.getChildCount() > 0) + { + fl_remote.removeAllViews(); + } + // Create render view by RtcEngine + surfaceView = RtcEngine.CreateRendererView(context); + surfaceView.setZOrderMediaOverlay(true); + // Add to the remote container + fl_remote.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + // Setup remote video to render + engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, uid)); + }); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) + { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + handler.post(new Runnable() { + @Override + public void run() { + /**Clear render view + Note: The video will stay at its last frame, to completely remove it you will need to + remove the SurfaceView from its parent*/ + engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); + } + }); + } + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/CustomRemoteVideoRender.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/CustomRemoteVideoRender.java new file mode 100644 index 000000000..055dbf6d5 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/CustomRemoteVideoRender.java @@ -0,0 +1,430 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.mediaio.AgoraSurfaceView; +import io.agora.rtc.mediaio.MediaIO; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; +import static io.agora.rtc.video.VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15; +import static io.agora.rtc.video.VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; +import static io.agora.rtc.video.VideoEncoderConfiguration.STANDARD_BITRATE; +import static io.agora.rtc.video.VideoEncoderConfiguration.VD_640x360; + +/** + * This example demonstrates how to customize the renderer to render the local scene of the remote video stream. + */ +@Example( + index = 9, + group = ADVANCED, + name = R.string.item_customremoterender, + actionId = R.id.action_mainFragment_to_CustomRemoteRender, + tipsId = R.string.customremoterender +) +public class CustomRemoteVideoRender extends BaseFragment implements View.OnClickListener { + private static final String TAG = CustomRemoteVideoRender.class.getSimpleName(); + + private FrameLayout fl_local, fl_remote; + private Button join; + private EditText et_channel; + private RtcEngine engine; + private int myUid; + private boolean joined = false; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_custom_remoterender, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + et_channel = view.findViewById(R.id.et_channel); + view.findViewById(R.id.btn_join).setOnClickListener(this); + fl_local = view.findViewById(R.id.fl_local); + fl_remote = view.findViewById(R.id.fl_remote); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) { + return; + } + try { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); + } + catch (Exception e) { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if (engine != null) { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.btn_join) { + if (!joined) { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE, + Permission.Group.CAMERA + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } else { + joined = false; + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + join.setText(getString(R.string.join)); + } + } + } + + private void joinChannel(String channelId) { + // Check if the context is valid + Context context = getContext(); + if (context == null) { + return; + } + + // Create render view by RtcEngine + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + // Add to the local container + if (fl_local.getChildCount() > 0) { + fl_local.removeAllViews(); + } + fl_local.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup local video to render your local camera preview + engine.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0)); + /**Set up to play remote sound with receiver*/ + engine.setDefaultAudioRoutetoSpeakerphone(false); + engine.setEnableSpeakerphone(false); + + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + // Enable video module + engine.enableVideo(); + // Setup video encoding configs + engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + ((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(), + VideoEncoderConfiguration.FRAME_RATE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate()), + STANDARD_BITRATE, + VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) + )); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0,option); + if (res != 0) { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() { + @Override + public void run() { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + } + }); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Since v2.9.0. + * Occurs when the remote video state changes. + * PS: This callback does not work properly when the number of users (in the Communication + * profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the remote user whose video state changes. + * @param state State of the remote video: + * REMOTE_VIDEO_STATE_STOPPED(0): The remote video is in the default state, probably due + * to REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3), REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5), + * or REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7). + * REMOTE_VIDEO_STATE_STARTING(1): The first remote video packet is received. + * REMOTE_VIDEO_STATE_DECODING(2): The remote video stream is decoded and plays normally, + * probably due to REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY (2), + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4), REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6), + * or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9). + * REMOTE_VIDEO_STATE_FROZEN(3): The remote video is frozen, probably due to + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1) or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8). + * REMOTE_VIDEO_STATE_FAILED(4): The remote video fails to start, probably due to + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0). + * @param reason The reason of the remote video state change: + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0): Internal reasons. + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3): The local user stops receiving the remote + * video stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote + * video stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5): The remote user stops sending the video + * stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the video + * stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8): The remote media stream falls back to the + * audio-only stream due to poor network conditions. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9): The remote media stream switches + * back to the video stream after the network conditions improve. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method until + * the SDK triggers this callback.*/ + @Override + public void onRemoteVideoStateChanged(int uid, int state, int reason, int elapsed) { + super.onRemoteVideoStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteVideoStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + handler.post(() -> + { + /**custom remote video surfaceView + * about AgoraSurfaceView,you can see this( + * https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1mediaio_1_1_agora_surface_view.html#ab4dcf3124f4bb4b945a043143babe41b)*/ + AgoraSurfaceView surfaceView = new AgoraSurfaceView(getContext()); + surfaceView.init(null); + surfaceView.setZOrderMediaOverlay(true); + surfaceView.setBufferType(MediaIO.BufferType.BYTE_BUFFER); + surfaceView.setPixelFormat(MediaIO.PixelFormat.I420); + if (fl_remote.getChildCount() > 0) { + fl_remote.removeAllViews(); + } + fl_remote.addView(surfaceView); + /**Customizes the remote video renderer. + * Call this method to add an external remote video renderer to the SDK. + * @param uid The ID of the remote user. + * @param render Sets the remote video renderer. See IVideoSink( + * https://docs.agora.io/en/Voice/API%20Reference/java/v3.0.1/interfaceio_1_1agora_1_1rtc_1_1mediaio_1_1_i_video_sink.html).*/ + engine.setRemoteVideoRenderer(uid, surfaceView); + }); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + handler.post(new Runnable() { + @Override + public void run() { + /**Clear render view + Note: The video will stay at its last frame, to completely remove it you will need to + remove the SurfaceView from its parent*/ + engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); + } + }); + } + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/GeoFencing.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/GeoFencing.java new file mode 100644 index 000000000..5ecf1ff12 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/GeoFencing.java @@ -0,0 +1,461 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.Spinner; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.RtcEngineConfig; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.rtc.RtcEngineConfig.AreaCode.AREA_CODE_AS; +import static io.agora.rtc.RtcEngineConfig.AreaCode.AREA_CODE_CN; +import static io.agora.rtc.RtcEngineConfig.AreaCode.AREA_CODE_EU; +import static io.agora.rtc.RtcEngineConfig.AreaCode.AREA_CODE_GLOB; +import static io.agora.rtc.RtcEngineConfig.AreaCode.AREA_CODE_IN; +import static io.agora.rtc.RtcEngineConfig.AreaCode.AREA_CODE_JP; +import static io.agora.rtc.RtcEngineConfig.AreaCode.AREA_CODE_NA; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; +import static io.agora.rtc.video.VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15; +import static io.agora.rtc.video.VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; +import static io.agora.rtc.video.VideoEncoderConfiguration.STANDARD_BITRATE; +import static io.agora.rtc.video.VideoEncoderConfiguration.VD_640x360; + +@Example( + index = 20, + group = ADVANCED, + name = R.string.item_geofencing, + actionId = R.id.action_mainFragment_to_GeoFencing, + tipsId = R.string.geofencing +) +public class GeoFencing extends BaseFragment implements View.OnClickListener { + private static final String TAG = GeoFencing.class.getSimpleName(); + + private FrameLayout fl_local, fl_remote; + private Button join; + private EditText et_channel; + private RtcEngine engine; + private int myUid; + private boolean joined = false; + private Spinner areaCode; + + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_geo_fencing, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + et_channel = view.findViewById(R.id.et_channel); + view.findViewById(R.id.btn_join).setOnClickListener(this); + fl_local = view.findViewById(R.id.fl_local); + fl_remote = view.findViewById(R.id.fl_remote); + areaCode = view.findViewById(R.id.areacode); + } + + private int getAreaCode() { + switch (areaCode.getSelectedItem().toString()) { + case "CN": + return AREA_CODE_CN; + case "NA": + return AREA_CODE_NA; + case "EU": + return AREA_CODE_EU; + case "AS": + return AREA_CODE_AS; + case "JP": + return AREA_CODE_JP; + case "IN": + return AREA_CODE_IN; + default: + return AREA_CODE_GLOB; + } + } + + private void initializeEngine() { + // Check if the context is valid + Context context = getContext(); + if (context == null || engine != null) { + return; + } + try { + RtcEngineConfig config = new RtcEngineConfig(); + config.mAppId = getString(R.string.agora_app_id); + config.mEventHandler = iRtcEngineEventHandler; + config.mContext = context.getApplicationContext(); + config.mAreaCode = getAreaCode(); + RtcEngineConfig.LogConfig logConfig = new RtcEngineConfig.LogConfig(); + // Log level set to ERROR + logConfig.level = Constants.LogLevel.getValue(Constants.LogLevel.LOG_LEVEL_ERROR); + // Log file size to 2MB + logConfig.fileSize = 2048; + config.mLogConfig = logConfig; + engine = RtcEngine.create(config); + } catch (Exception e) { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if (engine != null) { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.btn_join) { + if (!joined) { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE, + Permission.Group.CAMERA + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } else { + joined = false; + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + join.setText(getString(R.string.join)); + } + } + } + + private void joinChannel(String channelId) { + initializeEngine(); + // Check if the context is valid + Context context = getContext(); + if (context == null) { + return; + } + + // Create render view by RtcEngine + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + // Local video is on the top + if (fl_local.getChildCount() > 0) { + fl_local.removeAllViews(); + } + // Add to the local container + fl_local.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup local video to render your local camera preview + engine.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0)); + // Set audio route to microPhone + engine.setDefaultAudioRoutetoSpeakerphone(false); + + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + // Enable video module + engine.enableVideo(); + // Setup video encoding configs + engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + ((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(), + VideoEncoderConfiguration.FRAME_RATE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate()), + STANDARD_BITRATE, + VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) + )); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0,option); + if (res != 0) { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + if (err == 103) { + showLongToast("Current Area Code can't find server resources. Please try to set other area code."); + handler.post(() -> join.setEnabled(true)); + } else + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() { + @Override + public void run() { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + } + }); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Since v2.9.0. + * Occurs when the remote video state changes. + * PS: This callback does not work properly when the number of users (in the Communication + * profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the remote user whose video state changes. + * @param state State of the remote video: + * REMOTE_VIDEO_STATE_STOPPED(0): The remote video is in the default state, probably due + * to REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3), REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5), + * or REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7). + * REMOTE_VIDEO_STATE_STARTING(1): The first remote video packet is received. + * REMOTE_VIDEO_STATE_DECODING(2): The remote video stream is decoded and plays normally, + * probably due to REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY (2), + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4), REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6), + * or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9). + * REMOTE_VIDEO_STATE_FROZEN(3): The remote video is frozen, probably due to + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1) or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8). + * REMOTE_VIDEO_STATE_FAILED(4): The remote video fails to start, probably due to + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0). + * @param reason The reason of the remote video state change: + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0): Internal reasons. + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3): The local user stops receiving the remote + * video stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote + * video stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5): The remote user stops sending the video + * stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the video + * stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8): The remote media stream falls back to the + * audio-only stream due to poor network conditions. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9): The remote media stream switches + * back to the video stream after the network conditions improve. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method until + * the SDK triggers this callback.*/ + @Override + public void onRemoteVideoStateChanged(int uid, int state, int reason, int elapsed) { + super.onRemoteVideoStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteVideoStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + handler.post(() -> + { + /**Display remote video stream*/ + SurfaceView surfaceView = null; + if (fl_remote.getChildCount() > 0) { + fl_remote.removeAllViews(); + } + // Create render view by RtcEngine + surfaceView = RtcEngine.CreateRendererView(context); + surfaceView.setZOrderMediaOverlay(true); + // Add to the remote container + fl_remote.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + // Setup remote video to render + engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, uid)); + }); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + handler.post(new Runnable() { + @Override + public void run() { + /**Clear render view + Note: The video will stay at its last frame, to completely remove it you will need to + remove the SurfaceView from its parent*/ + engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); + } + }); + } + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/HostAcrossChannel.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/HostAcrossChannel.java new file mode 100644 index 000000000..9da40013f --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/HostAcrossChannel.java @@ -0,0 +1,536 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.video.ChannelMediaInfo; +import io.agora.rtc.video.ChannelMediaRelayConfiguration; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.api.example.common.model.Examples.BASIC; +import static io.agora.rtc.Constants.RELAY_STATE_CONNECTING; +import static io.agora.rtc.Constants.RELAY_STATE_FAILURE; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; +import static io.agora.rtc.video.VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15; +import static io.agora.rtc.video.VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; +import static io.agora.rtc.video.VideoEncoderConfiguration.STANDARD_BITRATE; +import static io.agora.rtc.video.VideoEncoderConfiguration.VD_640x360; + +/**This demo demonstrates how to make a one-to-one video call*/ +@Example( + index = 19, + group = ADVANCED, + name = R.string.item_hostacrosschannel, + actionId = R.id.action_mainFragment_to_hostacrosschannel, + tipsId = R.string.hostacrosschannel +) +public class HostAcrossChannel extends BaseFragment implements View.OnClickListener +{ + private static final String TAG = HostAcrossChannel.class.getSimpleName(); + + private FrameLayout fl_local, fl_remote; + private Button join, join_ex; + private EditText et_channel, et_channel_ex; + private RtcEngine engine; + private int myUid; + private boolean joined = false; + private boolean mediaRelaying = false; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_host_across_channel, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + join_ex = view.findViewById(R.id.btn_join_ex); + et_channel = view.findViewById(R.id.et_channel); + et_channel_ex = view.findViewById(R.id.et_channel_ex); + view.findViewById(R.id.btn_join).setOnClickListener(this); + view.findViewById(R.id.btn_join_ex).setOnClickListener(this); + fl_local = view.findViewById(R.id.fl_local); + fl_remote = view.findViewById(R.id.fl_remote); + join_ex.setEnabled(false); + et_channel_ex.setEnabled(false); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) + { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + try + { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); + } + catch (Exception e) + { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() + { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if(engine != null) + { + engine.leaveChannel(); + engine.stopChannelMediaRelay(); + mediaRelaying = false; + } + handler.post(RtcEngine::destroy); + engine = null; + } + + @Override + public void onClick(View v) + { + if (v.getId() == R.id.btn_join) + { + if (!joined) + { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) + { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE, + Permission.Group.CAMERA + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } + else + { + joined = false; + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + join.setText(getString(R.string.join)); + join_ex.setText(getString(R.string.join)); + } + } + else if(v.getId() == R.id.btn_join_ex){ + if(!mediaRelaying){ + String destChannelName = et_channel_ex.getText().toString(); + if(destChannelName.length() == 0){ + showAlert("Destination channel name is empty!"); + } + + ChannelMediaInfo srcChannelInfo = new ChannelMediaInfo(et_channel.getText().toString(), null, myUid); + ChannelMediaRelayConfiguration mediaRelayConfiguration = new ChannelMediaRelayConfiguration(); + mediaRelayConfiguration.setSrcChannelInfo(srcChannelInfo); + ChannelMediaInfo destChannelInfo = new ChannelMediaInfo(destChannelName, null, myUid); + mediaRelayConfiguration.setDestChannelInfo(destChannelName, destChannelInfo); + engine.startChannelMediaRelay(mediaRelayConfiguration); + et_channel_ex.setEnabled(false); + join_ex.setEnabled(false); + } + else{ + engine.stopChannelMediaRelay(); + et_channel_ex.setEnabled(true); + join_ex.setText(getString(R.string.join)); + mediaRelaying = false; + } + } + } + + private void joinChannel(String channelId) + { + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + + // Create render view by RtcEngine + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + if(fl_local.getChildCount() > 0) + { + fl_local.removeAllViews(); + } + // Add to the local container + fl_local.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup local video to render your local camera preview + engine.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0)); + // Set audio route to microPhone + engine.setDefaultAudioRoutetoSpeakerphone(false); + + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + // Enable video module + engine.enableVideo(); + // Setup video encoding configs + engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + ((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(), + VideoEncoderConfiguration.FRAME_RATE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate()), + STANDARD_BITRATE, + VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) + )); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) + { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) + { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() + { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) + { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) + { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) + { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) + { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() + { + @Override + public void run() + { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + join_ex.setEnabled(true); + et_channel_ex.setEnabled(true); + } + }); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Since v2.9.0. + * Occurs when the remote video state changes. + * PS: This callback does not work properly when the number of users (in the Communication + * profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the remote user whose video state changes. + * @param state State of the remote video: + * REMOTE_VIDEO_STATE_STOPPED(0): The remote video is in the default state, probably due + * to REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3), REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5), + * or REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7). + * REMOTE_VIDEO_STATE_STARTING(1): The first remote video packet is received. + * REMOTE_VIDEO_STATE_DECODING(2): The remote video stream is decoded and plays normally, + * probably due to REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY (2), + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4), REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6), + * or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9). + * REMOTE_VIDEO_STATE_FROZEN(3): The remote video is frozen, probably due to + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1) or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8). + * REMOTE_VIDEO_STATE_FAILED(4): The remote video fails to start, probably due to + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0). + * @param reason The reason of the remote video state change: + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0): Internal reasons. + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3): The local user stops receiving the remote + * video stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote + * video stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5): The remote user stops sending the video + * stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the video + * stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8): The remote media stream falls back to the + * audio-only stream due to poor network conditions. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9): The remote media stream switches + * back to the video stream after the network conditions improve. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method until + * the SDK triggers this callback.*/ + @Override + public void onRemoteVideoStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteVideoStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteVideoStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) + { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + handler.post(() -> + { + /**Display remote video stream*/ + SurfaceView surfaceView = null; + if (fl_remote.getChildCount() > 0) + { + fl_remote.removeAllViews(); + } + // Create render view by RtcEngine + surfaceView = RtcEngine.CreateRendererView(context); + surfaceView.setZOrderMediaOverlay(true); + // Add to the remote container + fl_remote.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + // Setup remote video to render + engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, uid)); + }); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) + { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + handler.post(new Runnable() { + @Override + public void run() { + /**Clear render view + Note: The video will stay at its last frame, to completely remove it you will need to + remove the SurfaceView from its parent*/ + engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); + } + }); + } + + /** + * Occurs when the state of the media stream relay changes. + * + * Since + * v2.9.0. + * The SDK reports the state of the current media relay and possible error messages in this callback. + * @param state The state code: + * RELAY_STATE_IDLE(0): The SDK is initializing. + * RELAY_STATE_CONNECTING(1): The SDK tries to relay the media stream to the destination channel. + * RELAY_STATE_RUNNING(2): The SDK successfully relays the media stream to the destination channel. + * RELAY_STATE_FAILURE(3): A failure occurs. See the details in code. + * @param code The error code + * RELAY_OK(0): The state is normal. + * RELAY_ERROR_SERVER_ERROR_RESPONSE(1): An error occurs in the server response. + * RELAY_ERROR_SERVER_NO_RESPONSE(2): No server response. You can call the leaveChannel method to leave the channel. + * RELAY_ERROR_NO_RESOURCE_AVAILABLE(3): The SDK fails to access the service, probably due to limited resources of the server. + * RELAY_ERROR_FAILED_JOIN_SRC(4): Fails to send the relay request. + * RELAY_ERROR_FAILED_JOIN_DEST(5): Fails to accept the relay request. + * RELAY_ERROR_FAILED_PACKET_RECEIVED_FROM_SRC(6): The server fails to receive the media stream. + * RELAY_ERROR_FAILED_PACKET_SENT_TO_DEST(7): The server fails to send the media stream. + * RELAY_ERROR_SERVER_CONNECTION_LOST(8): The SDK disconnects from the server due to poor network connections. You can call the leaveChannel method to leave the channel. + * RELAY_ERROR_INTERNAL_ERROR(9): An internal error occurs in the server. + * RELAY_ERROR_SRC_TOKEN_EXPIRED(10): The token of the source channel has expired. + * RELAY_ERROR_DEST_TOKEN_EXPIRED(11): The token of the destination channel has expired. + */ + @Override + public void onChannelMediaRelayStateChanged(int state, int code) { + switch (state){ + case RELAY_STATE_CONNECTING: + mediaRelaying = true; + handler.post(() ->{ + et_channel_ex.setEnabled(false); + join_ex.setEnabled(true); + join_ex.setText(getText(R.string.stop)); + showLongToast("channel media Relay connected."); + }); + break; + case RELAY_STATE_FAILURE: + mediaRelaying = false; + handler.post(() ->{ + showLongToast(String.format("channel media Relay failed at error code: %d", code)); + }); + } + } + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/InCallReport.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/InCallReport.java new file mode 100644 index 000000000..daf4a8c86 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/InCallReport.java @@ -0,0 +1,496 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.common.model.StatisticsInfo; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; +import static io.agora.rtc.video.VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15; +import static io.agora.rtc.video.VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; +import static io.agora.rtc.video.VideoEncoderConfiguration.STANDARD_BITRATE; +import static io.agora.rtc.video.VideoEncoderConfiguration.VD_640x360; + +@Example( + index = 17, + group = ADVANCED, + name = R.string.item_incallreport, + actionId = R.id.action_mainFragment_to_InCallReport, + tipsId = R.string.incallstats +) +public class InCallReport extends BaseFragment implements View.OnClickListener { + private static final String TAG = InCallReport.class.getSimpleName(); + + private FrameLayout fl_local, fl_remote; + private Button join; + private EditText et_channel; + private AppCompatTextView localStats, remoteStats; + private RtcEngine engine; + private StatisticsInfo statisticsInfo; + private int myUid; + private boolean joined = false; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_in_call_report, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + statisticsInfo = new StatisticsInfo(); + et_channel = view.findViewById(R.id.et_channel); + localStats = view.findViewById(R.id.local_stats); + localStats.bringToFront(); + remoteStats = view.findViewById(R.id.remote_stats); + remoteStats.bringToFront(); + view.findViewById(R.id.btn_join).setOnClickListener(this); + fl_local = view.findViewById(R.id.fl_local); + fl_remote = view.findViewById(R.id.fl_remote); + } + + private void updateLocalStats(){ + localStats.setText(statisticsInfo.getLocalVideoStats()); + } + + private void updateRemoteStats(){ + remoteStats.setText(statisticsInfo.getRemoteVideoStats()); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) + { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + try + { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); + } + catch (Exception e) + { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() + { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if(engine != null) + { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + } + + @Override + public void onClick(View v) + { + if (v.getId() == R.id.btn_join) + { + if (!joined) + { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) + { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE, + Permission.Group.CAMERA + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } + else + { + joined = false; + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + join.setText(getString(R.string.join)); + } + } + } + + private void joinChannel(String channelId) + { + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + + // Create render view by RtcEngine + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + if(fl_local.getChildCount() > 0) + { + fl_local.removeAllViews(); + } + // Add to the local container + fl_local.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup local video to render your local camera preview + engine.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0)); + // Set audio route to microPhone + engine.setDefaultAudioRoutetoSpeakerphone(false); + + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + // Enable video module + engine.enableVideo(); + // Setup video encoding configs + engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + ((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(), + VideoEncoderConfiguration.FRAME_RATE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate()), + STANDARD_BITRATE, + VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) + )); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) + { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) + { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() + { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) + { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) + { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) + { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) + { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() + { + @Override + public void run() + { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + } + }); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Since v2.9.0. + * Occurs when the remote video state changes. + * PS: This callback does not work properly when the number of users (in the Communication + * profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the remote user whose video state changes. + * @param state State of the remote video: + * REMOTE_VIDEO_STATE_STOPPED(0): The remote video is in the default state, probably due + * to REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3), REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5), + * or REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7). + * REMOTE_VIDEO_STATE_STARTING(1): The first remote video packet is received. + * REMOTE_VIDEO_STATE_DECODING(2): The remote video stream is decoded and plays normally, + * probably due to REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY (2), + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4), REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6), + * or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9). + * REMOTE_VIDEO_STATE_FROZEN(3): The remote video is frozen, probably due to + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1) or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8). + * REMOTE_VIDEO_STATE_FAILED(4): The remote video fails to start, probably due to + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0). + * @param reason The reason of the remote video state change: + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0): Internal reasons. + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3): The local user stops receiving the remote + * video stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote + * video stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5): The remote user stops sending the video + * stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the video + * stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8): The remote media stream falls back to the + * audio-only stream due to poor network conditions. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9): The remote media stream switches + * back to the video stream after the network conditions improve. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method until + * the SDK triggers this callback.*/ + @Override + public void onRemoteVideoStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteVideoStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteVideoStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) + { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + handler.post(() -> + { + /**Display remote video stream*/ + SurfaceView surfaceView = null; + if (fl_remote.getChildCount() > 0) + { + fl_remote.removeAllViews(); + } + // Create render view by RtcEngine + surfaceView = RtcEngine.CreateRendererView(context); + surfaceView.setZOrderMediaOverlay(true); + // Add to the remote container + fl_remote.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + // Setup remote video to render + engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, uid)); + }); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) + { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + handler.post(new Runnable() { + @Override + public void run() { + /**Clear render view + Note: The video will stay at its last frame, to completely remove it you will need to + remove the SurfaceView from its parent*/ + engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); + } + }); + } + + @Override + public void onRemoteAudioStats(RemoteAudioStats remoteAudioStats) { + statisticsInfo.setRemoteAudioStats(remoteAudioStats); + updateRemoteStats(); + } + + @Override + public void onLocalAudioStats(LocalAudioStats localAudioStats) { + statisticsInfo.setLocalAudioStats(localAudioStats); + updateLocalStats(); + } + + @Override + public void onRemoteVideoStats(RemoteVideoStats remoteVideoStats) { + statisticsInfo.setRemoteVideoStats(remoteVideoStats); + updateRemoteStats(); + } + + @Override + public void onLocalVideoStats(LocalVideoStats localVideoStats) { + statisticsInfo.setLocalVideoStats(localVideoStats); + updateLocalStats(); + } + + @Override + public void onRtcStats(RtcStats rtcStats) { + statisticsInfo.setRtcStats(rtcStats); + } + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/JoinMultipleChannel.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/JoinMultipleChannel.java new file mode 100644 index 000000000..6d6b754d1 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/JoinMultipleChannel.java @@ -0,0 +1,544 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.examples.basic.JoinChannelVideo; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcChannelEventHandler; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcChannel; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_FIT; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; +import static io.agora.rtc.video.VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15; +import static io.agora.rtc.video.VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; +import static io.agora.rtc.video.VideoEncoderConfiguration.STANDARD_BITRATE; +import static io.agora.rtc.video.VideoEncoderConfiguration.VD_640x360; + +@Example( + index = 12, + group = ADVANCED, + name = R.string.item_joinmultichannel, + actionId = R.id.action_mainFragment_to_MultiChannel, + tipsId = R.string.joinmultichannel +) +public class JoinMultipleChannel extends BaseFragment implements View.OnClickListener { + private static final String TAG = JoinChannelVideo.class.getSimpleName(); + + private FrameLayout fl_local, fl_remote, fl_remote2; + private Button join; + private EditText et_channel; + private RtcEngine engine; + private int myUid; + private boolean joined = false; + private String channel1; + private String channel2; + private RtcChannel rtcChannel; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_join_multi_channel, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + et_channel = view.findViewById(R.id.et_channel); + view.findViewById(R.id.btn_join).setOnClickListener(this); + fl_local = view.findViewById(R.id.fl_local); + fl_remote = view.findViewById(R.id.fl_remote); + fl_remote2 = view.findViewById(R.id.fl_remote2); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) + { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + try + { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); + } + catch (Exception e) + { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() + { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if(engine != null) + { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + } + + @Override + public void onClick(View v) + { + if (v.getId() == R.id.btn_join) + { + if (!joined) + { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + channel1 = et_channel.getText().toString(); + channel2 = channel1 + "-2"; + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) + { + joinChannel(channel1); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE, + Permission.Group.CAMERA + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channel1); + }).start(); + } + else + { + joined = false; + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + rtcChannel.leaveChannel(); + join.setText(getString(R.string.join)); + } + } + } + + private void joinChannel(String channelId) + { + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + + // Create render view by RtcEngine + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + if(fl_local.getChildCount() > 0) + { + fl_local.removeAllViews(); + } + // Add to the local container + fl_local.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup local video to render your local camera preview + engine.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0)); + // Set audio route to microPhone + engine.setDefaultAudioRoutetoSpeakerphone(false); + + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + // Enable video module + engine.enableVideo(); + // Setup video encoding configs + engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + ((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(), + VideoEncoderConfiguration.FRAME_RATE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate()), + STANDARD_BITRATE, + VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) + )); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) + { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0 || !joinSecondChannel()) + { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + private boolean joinSecondChannel() { + // 1. Create rtcChannel + rtcChannel = engine.createRtcChannel(channel2); + // 2. Set rtcChannelEventHandler + rtcChannel.setRtcChannelEventHandler(new IRtcChannelEventHandler() { + // Override events + /** + * Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * + * @param rtcChannel Channel object + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered + */ + @Override + public void onJoinChannelSuccess(RtcChannel rtcChannel, int uid, int elapsed) { + super.onJoinChannelSuccess(rtcChannel, uid, elapsed); + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel2, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel2, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() { + @Override + public void run() { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + } + }); + } + /** + * Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered. + */ + @Override + public void onUserJoined(RtcChannel rtcChannel, int uid, int elapsed) { + super.onUserJoined(rtcChannel, uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + handler.post(() -> + { + /**Display remote video stream*/ + SurfaceView surfaceView = null; + if (fl_remote2.getChildCount() > 0) { + fl_remote2.removeAllViews(); + } + // Create render view by RtcEngine + surfaceView = RtcEngine.CreateRendererView(context); + surfaceView.setZOrderMediaOverlay(true); + // Add to the remote container + fl_remote2.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + // Setup remote video to render + engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_FIT, channel2, uid)); + }); + } + }); + // 3. Configurate mediaOptions + ChannelMediaOptions mediaOptions = new ChannelMediaOptions(); + mediaOptions.autoSubscribeAudio = true; + mediaOptions.autoSubscribeVideo = true; + // 4. Join channel + int ret = rtcChannel.joinChannel(null, "", 0, mediaOptions); + return (ret == 0); + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { + /** + * Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html + */ + @Override + public void onWarning(int warn) { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /** + * Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + */ + @Override + public void onError(int err) { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /** + * Occurs when a user leaves the channel. + * + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics. + */ + @Override + public void onLeaveChannel(RtcStats stats) { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /** + * Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered + */ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() { + @Override + public void run() { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + } + }); + } + + /** + * Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback. + */ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /** + * Since v2.9.0. + * Occurs when the remote video state changes. + * PS: This callback does not work properly when the number of users (in the Communication + * profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * + * @param uid ID of the remote user whose video state changes. + * @param state State of the remote video: + * REMOTE_VIDEO_STATE_STOPPED(0): The remote video is in the default state, probably due + * to REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3), REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5), + * or REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7). + * REMOTE_VIDEO_STATE_STARTING(1): The first remote video packet is received. + * REMOTE_VIDEO_STATE_DECODING(2): The remote video stream is decoded and plays normally, + * probably due to REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY (2), + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4), REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6), + * or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9). + * REMOTE_VIDEO_STATE_FROZEN(3): The remote video is frozen, probably due to + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1) or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8). + * REMOTE_VIDEO_STATE_FAILED(4): The remote video fails to start, probably due to + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0). + * @param reason The reason of the remote video state change: + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0): Internal reasons. + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3): The local user stops receiving the remote + * video stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote + * video stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5): The remote user stops sending the video + * stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the video + * stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8): The remote media stream falls back to the + * audio-only stream due to poor network conditions. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9): The remote media stream switches + * back to the video stream after the network conditions improve. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method until + * the SDK triggers this callback. + */ + @Override + public void onRemoteVideoStateChanged(int uid, int state, int reason, int elapsed) { + super.onRemoteVideoStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteVideoStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /** + * Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered. + */ + @Override + public void onUserJoined(int uid, int elapsed) { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + handler.post(() -> + { + /**Display remote video stream*/ + SurfaceView surfaceView = null; + if (fl_remote.getChildCount() > 0) { + fl_remote.removeAllViews(); + } + // Create render view by RtcEngine + surfaceView = RtcEngine.CreateRendererView(context); + surfaceView.setZOrderMediaOverlay(true); + // Add to the remote container + fl_remote.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + // Setup remote video to render + engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_FIT, uid)); + }); + } + + /** + * Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience. + */ + @Override + public void onUserOffline(int uid, int reason) { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + handler.post(new Runnable() { + @Override + public void run() { + /**Clear render view + Note: The video will stay at its last frame, to completely remove it you will need to + remove the SurfaceView from its parent*/ + engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); + } + }); + } + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/LiveStreaming.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/LiveStreaming.java new file mode 100644 index 000000000..3df80bdfb --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/LiveStreaming.java @@ -0,0 +1,507 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.models.ClientRoleOptions; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; +import static io.agora.rtc.video.VideoEncoderConfiguration.STANDARD_BITRATE; + +/** + * This demo demonstrates how to make a one-to-one video call + */ +@Example( + index = 23, + group = ADVANCED, + name = R.string.item_livestreaming, + actionId = R.id.action_mainFragment_to_live_streaming, + tipsId = R.string.livestreaming +) +public class LiveStreaming extends BaseFragment implements View.OnClickListener { + private static final String TAG = LiveStreaming.class.getSimpleName(); + + private FrameLayout foreGroundVideo, backGroundVideo; + private Button join, publish, latency; + private EditText et_channel; + private RtcEngine engine; + private int myUid; + private int remoteUid; + private boolean joined = false; + private boolean isHost = false; + private boolean isLowLatency = false; + private boolean isLocalVideoForeground = false; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_live_streaming, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + publish = view.findViewById(R.id.btn_publish); + latency = view.findViewById(R.id.btn_latency); + et_channel = view.findViewById(R.id.et_channel); + latency.setEnabled(false); + publish.setEnabled(false); + view.findViewById(R.id.btn_join).setOnClickListener(this); + view.findViewById(R.id.btn_publish).setOnClickListener(this); + view.findViewById(R.id.btn_latency).setOnClickListener(this); + view.findViewById(R.id.foreground_video).setOnClickListener(this); + foreGroundVideo = view.findViewById(R.id.background_video); + backGroundVideo = view.findViewById(R.id.foreground_video); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) { + return; + } + try { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); + } catch (Exception e) { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if (engine != null) { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.btn_join) { + if (!joined) { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE, + Permission.Group.CAMERA + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } else { + joined = false; + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + join.setText(getString(R.string.join)); + } + } else if (v.getId() == R.id.btn_publish) { + isHost = !isHost; + if(isHost){ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + } + else{ + ClientRoleOptions clientRoleOptions = new ClientRoleOptions(); + clientRoleOptions.audienceLatencyLevel = isLowLatency ? Constants.AUDIENCE_LATENCY_LEVEL_ULTRA_LOW_LATENCY : Constants.AUDIENCE_LATENCY_LEVEL_LOW_LATENCY; + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_AUDIENCE, clientRoleOptions); + } + publish.setEnabled(false); + publish.setText(isHost ? getString(R.string.disnable_publish) : getString(R.string.enable_publish)); + + } else if (v.getId() == R.id.btn_latency) { + isLowLatency = !isLowLatency; + latency.setText(isLowLatency ? getString(R.string.disable_low_latency) : getString(R.string.enable_low_latency)); + } else if (v.getId() == R.id.foreground_video) { + isLocalVideoForeground = !isLocalVideoForeground; + if (foreGroundVideo.getChildCount() > 0) { + foreGroundVideo.removeAllViews(); + } + if (backGroundVideo.getChildCount() > 0) { + backGroundVideo.removeAllViews(); + } + // Create render view by RtcEngine + SurfaceView localView = RtcEngine.CreateRendererView(getContext()); + SurfaceView remoteView = RtcEngine.CreateRendererView(getContext()); + if (isLocalVideoForeground){ + // Add to the local container + foreGroundVideo.addView(localView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Add to the remote container + backGroundVideo.addView(remoteView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup remote video to render + engine.setupRemoteVideo(new VideoCanvas(remoteView, RENDER_MODE_HIDDEN, remoteUid)); + // Setup local video to render your local camera preview + engine.setupLocalVideo(new VideoCanvas(localView, RENDER_MODE_HIDDEN, 0)); + remoteView.setZOrderMediaOverlay(true); + remoteView.setZOrderOnTop(true); + } + else{ + // Add to the local container + foreGroundVideo.addView(remoteView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Add to the remote container + backGroundVideo.addView(localView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup local video to render your local camera preview + engine.setupLocalVideo(new VideoCanvas(localView, RENDER_MODE_HIDDEN, 0)); + // Setup remote video to render + engine.setupRemoteVideo(new VideoCanvas(remoteView, RENDER_MODE_HIDDEN, remoteUid)); + localView.setZOrderMediaOverlay(true); + localView.setZOrderOnTop(true); + } + } + + } + + private void joinChannel(String channelId) { + // Check if the context is valid + Context context = getContext(); + if (context == null) { + return; + } + + // Create render view by RtcEngine + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + if (foreGroundVideo.getChildCount() > 0) { + foreGroundVideo.removeAllViews(); + } + // Add to the local container + foreGroundVideo.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup local video to render your local camera preview + engine.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0)); + // Set audio route to microPhone + engine.setDefaultAudioRoutetoSpeakerphone(false); + + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_AUDIENCE); + // Enable video module + engine.enableVideo(); + // Setup video encoding configs + engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + ((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(), + VideoEncoderConfiguration.FRAME_RATE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate()), + STANDARD_BITRATE, + VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) + )); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() { + @Override + public void run() { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + publish.setEnabled(true); + latency.setEnabled(true); + } + }); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Since v2.9.0. + * Occurs when the remote video state changes. + * PS: This callback does not work properly when the number of users (in the Communication + * profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the remote user whose video state changes. + * @param state State of the remote video: + * REMOTE_VIDEO_STATE_STOPPED(0): The remote video is in the default state, probably due + * to REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3), REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5), + * or REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7). + * REMOTE_VIDEO_STATE_STARTING(1): The first remote video packet is received. + * REMOTE_VIDEO_STATE_DECODING(2): The remote video stream is decoded and plays normally, + * probably due to REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY (2), + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4), REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6), + * or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9). + * REMOTE_VIDEO_STATE_FROZEN(3): The remote video is frozen, probably due to + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1) or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8). + * REMOTE_VIDEO_STATE_FAILED(4): The remote video fails to start, probably due to + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0). + * @param reason The reason of the remote video state change: + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0): Internal reasons. + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3): The local user stops receiving the remote + * video stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote + * video stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5): The remote user stops sending the video + * stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the video + * stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8): The remote media stream falls back to the + * audio-only stream due to poor network conditions. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9): The remote media stream switches + * back to the video stream after the network conditions improve. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method until + * the SDK triggers this callback.*/ + @Override + public void onRemoteVideoStateChanged(int uid, int state, int reason, int elapsed) { + super.onRemoteVideoStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteVideoStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + if(remoteUid != 0) { + return; + } + else{ + remoteUid = uid; + } + handler.post(() -> + { + /**Display remote video stream*/ + SurfaceView surfaceView = null; + if (backGroundVideo.getChildCount() > 0) { + backGroundVideo.removeAllViews(); + } + // Create render view by RtcEngine + surfaceView = RtcEngine.CreateRendererView(context); + surfaceView.setZOrderMediaOverlay(true); + // Add to the remote container + backGroundVideo.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + // Setup remote video to render + engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, remoteUid)); + }); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + handler.post(new Runnable() { + @Override + public void run() { + /**Clear render view + Note: The video will stay at its last frame, to completely remove it you will need to + remove the SurfaceView from its parent*/ + engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); + } + }); + } + + /** + * Occurs when the user role switches in a live streaming. For example, from a host to an audience or vice versa. + * + * The SDK triggers this callback when the local user switches the user role by calling the setClientRole method after joining the channel. + * @param oldRole Role that the user switches from. + * @param newRole Role that the user switches to. + */ + @Override + public void onClientRoleChanged(int oldRole, int newRole) { + Log.i(TAG, String.format("client role changed from state %d to %d", oldRole, newRole)); handler.post(new Runnable() { + @Override + public void run() { + publish.setEnabled(true); + } + }); + } + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MediaPlayerKit.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MediaPlayerKit.java new file mode 100644 index 000000000..ab5136658 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MediaPlayerKit.java @@ -0,0 +1,580 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.SeekBar; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import io.agora.RtcChannelPublishHelper; +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.mediaplayer.AgoraMediaPlayerKit; +import io.agora.mediaplayer.AudioFrameObserver; +import io.agora.mediaplayer.Constants; +import io.agora.mediaplayer.MediaPlayerObserver; +import io.agora.mediaplayer.VideoFrameObserver; +import io.agora.mediaplayer.data.AudioFrame; +import io.agora.mediaplayer.data.VideoFrame; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.mediaio.AgoraDefaultSource; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; +import io.agora.utils.LogUtil; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.mediaplayer.Constants.MediaPlayerState.PLAYER_STATE_OPEN_COMPLETED; +import static io.agora.mediaplayer.Constants.MediaPlayerState.PLAYER_STATE_PLAYING; +import static io.agora.mediaplayer.Constants.PLAYER_RENDER_MODE_FIT; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; +import static io.agora.rtc.video.VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15; +import static io.agora.rtc.video.VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; +import static io.agora.rtc.video.VideoEncoderConfiguration.STANDARD_BITRATE; +import static io.agora.rtc.video.VideoEncoderConfiguration.VD_640x360; + +@Example( + index = 16, + group = ADVANCED, + name = R.string.item_mediaplayerkit, + actionId = R.id.action_mainFragment_to_MediaPlayerKit, + tipsId = R.string.mediaplayerkit +) +public class MediaPlayerKit extends BaseFragment implements View.OnClickListener { + + private static final String TAG = MediaPlayerKit.class.getSimpleName(); + + private Button join, open, play, stop, pause, publish, unpublish; + private EditText et_channel, et_url; + private RtcEngine engine; + private int myUid; + private FrameLayout fl_local, fl_remote; + + private AgoraMediaPlayerKit agoraMediaPlayerKit; + private boolean joined = false; + private SeekBar progressBar, volumeBar; + private long playerDuration = 0; + + private static final String SAMPLE_MOVIE_URL = "https://webdemo.agora.io/agora-web-showcase/examples/Agora-Custom-VideoSource-Web/assets/sample.mp4"; + + RtcChannelPublishHelper rtcChannelPublishHelper = RtcChannelPublishHelper.getInstance(); + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_media_player_kit, container, false); + return view; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) { + return; + } + try { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); + } catch (Exception e) { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + open = view.findViewById(R.id.open); + play = view.findViewById(R.id.play); + stop = view.findViewById(R.id.stop); + pause = view.findViewById(R.id.pause); + publish = view.findViewById(R.id.publish); + unpublish = view.findViewById(R.id.unpublish); + progressBar = view.findViewById(R.id.ctrl_progress_bar); + progressBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + + } + + }); + volumeBar = view.findViewById(R.id.ctrl_volume_bar); + volumeBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int i, boolean b) { + agoraMediaPlayerKit.adjustPlayoutVolume(i); + rtcChannelPublishHelper.adjustPublishSignalVolume(i,i); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + + } + }); + et_channel = view.findViewById(R.id.et_channel); + et_url = view.findViewById(R.id.link); + et_url.setText(SAMPLE_MOVIE_URL); + view.findViewById(R.id.btn_join).setOnClickListener(this); + view.findViewById(R.id.open).setOnClickListener(this); + view.findViewById(R.id.play).setOnClickListener(this); + view.findViewById(R.id.stop).setOnClickListener(this); + view.findViewById(R.id.pause).setOnClickListener(this); + view.findViewById(R.id.publish).setOnClickListener(this); + view.findViewById(R.id.unpublish).setOnClickListener(this); + fl_local = view.findViewById(R.id.fl_local); + fl_remote = view.findViewById(R.id.fl_remote); + agoraMediaPlayerKit = new AgoraMediaPlayerKit(this.getActivity()); + agoraMediaPlayerKit.registerPlayerObserver(new MediaPlayerObserver() { + @Override + public void onPlayerStateChanged(Constants.MediaPlayerState state, Constants.MediaPlayerError error) { + LogUtil.i("agoraMediaPlayerKit1 onPlayerStateChanged:" + state + " " + error); + if (state.equals(PLAYER_STATE_OPEN_COMPLETED)) { + play.setEnabled(true); + stop.setEnabled(true); + pause.setEnabled(true); + publish.setEnabled(true); + unpublish.setEnabled(true); + } + } + + + @Override + public void onPositionChanged(final long position) { + if (playerDuration > 0) { + final int result = (int) ((float) position / (float) playerDuration * 100); + handler.post(new Runnable() { + @Override + public void run() { + progressBar.setProgress(Long.valueOf(result).intValue()); + } + }); + } + } + + + @Override + public void onMetaData(Constants.MediaPlayerMetadataType mediaPlayerMetadataType, byte[] bytes) { + + } + + @Override + public void onPlayBufferUpdated(long l) { + + } + + @Override + public void onPlayerEvent(Constants.MediaPlayerEvent eventCode) { + LogUtil.i("agoraMediaPlayerKit1 onEvent:" + eventCode); + } + + }); + agoraMediaPlayerKit.registerVideoFrameObserver(new VideoFrameObserver() { + @Override + public void onFrame(VideoFrame videoFrame) { + LogUtil.i("agoraMediaPlayerKit1 video onFrame :" + videoFrame); + } + }); + agoraMediaPlayerKit.registerAudioFrameObserver(new AudioFrameObserver() { + @Override + public void onFrame(AudioFrame audioFrame) { + LogUtil.i("agoraMediaPlayerKit1 audio onFrame :" + audioFrame); + } + }); + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.btn_join) { + if (!joined) { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE, + Permission.Group.CAMERA + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } else { + joined = false; + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + join.setText(getString(R.string.join)); + agoraMediaPlayerKit.stop(); + agoraMediaPlayerKit.destroy(); + open.setEnabled(false); + play.setEnabled(false); + stop.setEnabled(false); + pause.setEnabled(false); + publish.setEnabled(false); + unpublish.setEnabled(false); + } + } else if (v.getId() == R.id.open) { + String url = et_url.getText().toString(); + if (url != null && !"".equals(url)) { + agoraMediaPlayerKit.open(url, 0); + progressBar.setVisibility(View.VISIBLE); + volumeBar.setVisibility(View.VISIBLE); + volumeBar.setProgress(100); + } + } else if (v.getId() == R.id.play) { + agoraMediaPlayerKit.play(); + playerDuration = agoraMediaPlayerKit.getDuration(); + } else if (v.getId() == R.id.stop) { + agoraMediaPlayerKit.stop(); + } else if (v.getId() == R.id.pause) { + agoraMediaPlayerKit.pause(); + } else if (v.getId() == R.id.publish) { + rtcChannelPublishHelper.publishAudio(); + rtcChannelPublishHelper.publishVideo(); + } else if (v.getId() == R.id.unpublish) { + rtcChannelPublishHelper.unpublishAudio(); + rtcChannelPublishHelper.unpublishVideo(); + } + } + + private void joinChannel(String channelId) { + // Check if the context is valid + Context context = getContext(); + if (context == null) { + return; + } + + engine.setDefaultAudioRoutetoSpeakerphone(false); + + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(io.agora.rtc.Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + // Enable video module + engine.enableVideo(); + // Setup video encoding configs + engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + ((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(), + VideoEncoderConfiguration.FRAME_RATE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate()), + STANDARD_BITRATE, + VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) + )); + + SurfaceView surfaceView = new SurfaceView(this.getActivity()); + surfaceView.setZOrderMediaOverlay(false); + if (fl_local.getChildCount() > 0) { + fl_local.removeAllViews(); + } + fl_local.addView(surfaceView); + + // attach player to agora rtc kit, so that the media stream can be published + rtcChannelPublishHelper.attachPlayerToRtc(agoraMediaPlayerKit, engine); + + // set media local play view + agoraMediaPlayerKit.setView(surfaceView); + agoraMediaPlayerKit.setRenderMode(PLAYER_RENDER_MODE_FIT); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(() -> { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + open.setEnabled(true); + }); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Since v2.9.0. + * Occurs when the remote video state changes. + * PS: This callback does not work properly when the number of users (in the Communication + * profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the remote user whose video state changes. + * @param state State of the remote video: + * REMOTE_VIDEO_STATE_STOPPED(0): The remote video is in the default state, probably due + * to REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3), REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5), + * or REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7). + * REMOTE_VIDEO_STATE_STARTING(1): The first remote video packet is received. + * REMOTE_VIDEO_STATE_DECODING(2): The remote video stream is decoded and plays normally, + * probably due to REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY (2), + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4), REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6), + * or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9). + * REMOTE_VIDEO_STATE_FROZEN(3): The remote video is frozen, probably due to + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1) or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8). + * REMOTE_VIDEO_STATE_FAILED(4): The remote video fails to start, probably due to + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0). + * @param reason The reason of the remote video state change: + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0): Internal reasons. + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3): The local user stops receiving the remote + * video stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote + * video stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5): The remote user stops sending the video + * stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the video + * stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8): The remote media stream falls back to the + * audio-only stream due to poor network conditions. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9): The remote media stream switches + * back to the video stream after the network conditions improve. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method until + * the SDK triggers this callback.*/ + @Override + public void onRemoteVideoStateChanged(int uid, int state, int reason, int elapsed) { + super.onRemoteVideoStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteVideoStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + handler.post(() -> + { + /**Display remote video stream*/ + SurfaceView surfaceView = null; + if (fl_remote.getChildCount() > 0) { + fl_remote.removeAllViews(); + } + // Create render view by RtcEngine + surfaceView = RtcEngine.CreateRendererView(context); + surfaceView.setZOrderMediaOverlay(true); + // Add to the remote container + fl_remote.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + // Setup remote video to render + engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, uid)); + }); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + handler.post(new Runnable() { + @Override + public void run() { + /**Clear render view + Note: The video will stay at its last frame, to completely remove it you will need to + remove the SurfaceView from its parent*/ + engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); + } + }); + } + }; + + @Override + public void onDestroy() { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + agoraMediaPlayerKit.destroy(); + if (engine != null) { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + } + +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MultiProcess.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MultiProcess.java new file mode 100644 index 000000000..f562e8817 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/MultiProcess.java @@ -0,0 +1,509 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.ss.ScreenSharingClient; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; +import static io.agora.rtc.video.VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15; +import static io.agora.rtc.video.VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; +import static io.agora.rtc.video.VideoEncoderConfiguration.STANDARD_BITRATE; +import static io.agora.rtc.video.VideoEncoderConfiguration.VD_640x360; + +/**This demo demonstrates how to make a one-to-one video call*/ +@Example( + index = 23, + group = ADVANCED, + name = R.string.item_twoProcessScreenShare, + actionId = R.id.action_mainFragment_to_two_process_screen_share, + tipsId = R.string.multiProcessScreenShare +) +public class MultiProcess extends BaseFragment implements View.OnClickListener +{ + private static final String TAG = MultiProcess.class.getSimpleName(); + private static final Integer SCREEN_SHARE_UID = 10000; + + private FrameLayout fl_local, fl_remote; + private Button join, screenShare; + private EditText et_channel; + private RtcEngine engine; + private int myUid; + private boolean joined = false; + private boolean isSharing = false; + private ScreenSharingClient mSSClient; + + private final ScreenSharingClient.IStateListener mListener = new ScreenSharingClient.IStateListener() { + @Override + public void onError(int error) { + Log.e(TAG, "Screen share service error happened: " + error); + } + + @Override + public void onTokenWillExpire() { + Log.d(TAG, "Screen share service token will expire"); + mSSClient.renewToken(null); // Replace the token with your valid token + } + }; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_two_process_screen_share, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + screenShare = view.findViewById(R.id.screenShare); + screenShare.setEnabled(false); + et_channel = view.findViewById(R.id.et_channel); + view.findViewById(R.id.btn_join).setOnClickListener(this); + view.findViewById(R.id.screenShare).setOnClickListener(this); + fl_local = view.findViewById(R.id.fl_local); + fl_remote = view.findViewById(R.id.fl_remote); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) + { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + try + { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); + + // Initialize Screen Share Client + mSSClient = ScreenSharingClient.getInstance(); + mSSClient.setListener(mListener); + } + catch (Exception e) + { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() + { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if(engine != null) + { + engine.leaveChannel(); + } + if (isSharing) { + mSSClient.stop(getContext()); + } + handler.post(RtcEngine::destroy); + engine = null; + } + + @Override + public void onClick(View v) + { + if (v.getId() == R.id.btn_join) + { + if (!joined) + { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) + { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE, + Permission.Group.CAMERA + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } + else + { + joined = false; + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + join.setText(getString(R.string.join)); + mSSClient.stop(getContext()); + screenShare.setText(getResources().getString(R.string.screenshare)); + screenShare.setEnabled(false); + isSharing = false; + } + } + else if (v.getId() == R.id.screenShare){ + String channelId = et_channel.getText().toString(); + if (!isSharing) { + mSSClient.start(getContext(), getResources().getString(R.string.agora_app_id), null, + channelId, SCREEN_SHARE_UID, new VideoEncoderConfiguration( + VD_640x360, + FRAME_RATE_FPS_15, + STANDARD_BITRATE, + ORIENTATION_MODE_ADAPTIVE + )); + screenShare.setText(getResources().getString(R.string.stop)); + isSharing = true; + } else { + mSSClient.stop(getContext()); + screenShare.setText(getResources().getString(R.string.screenshare)); + isSharing = false; + } + } + } + + private void joinChannel(String channelId) + { + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + + // Create render view by RtcEngine + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + if(fl_local.getChildCount() > 0) + { + fl_local.removeAllViews(); + } + // Add to the local container + fl_local.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup local video to render your local camera preview + engine.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0)); + // Set audio route to microPhone + engine.setDefaultAudioRoutetoSpeakerphone(false); + + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + // Enable video module + engine.enableVideo(); + // Setup video encoding configs + engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + ((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(), + VideoEncoderConfiguration.FRAME_RATE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate()), + STANDARD_BITRATE, + VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) + )); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) + { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) + { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() + { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) + { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) + { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) + { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) + { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() + { + @Override + public void run() + { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + screenShare.setEnabled(true); + } + }); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Since v2.9.0. + * Occurs when the remote video state changes. + * PS: This callback does not work properly when the number of users (in the Communication + * profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the remote user whose video state changes. + * @param state State of the remote video: + * REMOTE_VIDEO_STATE_STOPPED(0): The remote video is in the default state, probably due + * to REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3), REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5), + * or REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7). + * REMOTE_VIDEO_STATE_STARTING(1): The first remote video packet is received. + * REMOTE_VIDEO_STATE_DECODING(2): The remote video stream is decoded and plays normally, + * probably due to REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY (2), + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4), REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6), + * or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9). + * REMOTE_VIDEO_STATE_FROZEN(3): The remote video is frozen, probably due to + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1) or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8). + * REMOTE_VIDEO_STATE_FAILED(4): The remote video fails to start, probably due to + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0). + * @param reason The reason of the remote video state change: + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0): Internal reasons. + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3): The local user stops receiving the remote + * video stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote + * video stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5): The remote user stops sending the video + * stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the video + * stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8): The remote media stream falls back to the + * audio-only stream due to poor network conditions. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9): The remote media stream switches + * back to the video stream after the network conditions improve. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method until + * the SDK triggers this callback.*/ + @Override + public void onRemoteVideoStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteVideoStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteVideoStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) + { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + // don't render screen sharing view + if (SCREEN_SHARE_UID == uid){ + return; + } + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + handler.post(() -> + { + /**Display remote video stream*/ + SurfaceView surfaceView = null; + if (fl_remote.getChildCount() > 0) + { + fl_remote.removeAllViews(); + } + // Create render view by RtcEngine + surfaceView = RtcEngine.CreateRendererView(context); + surfaceView.setZOrderMediaOverlay(true); + // Add to the remote container + fl_remote.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + // Setup remote video to render + engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, uid)); + }); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) + { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + if (SCREEN_SHARE_UID == uid){ + return; + } + handler.post(new Runnable() { + @Override + public void run() { + /**Clear render view + Note: The video will stay at its last frame, to completely remove it you will need to + remove the SurfaceView from its parent*/ + engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); + } + }); + } + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PlayAudioFiles.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PlayAudioFiles.java new file mode 100644 index 000000000..55a2503b3 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PlayAudioFiles.java @@ -0,0 +1,478 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.SeekBar; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import io.agora.api.component.Constant; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IAudioEffectManager; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.models.ChannelMediaOptions; + +import static io.agora.api.example.common.model.Examples.ADVANCED; + +@Example( + index = 14, + group = ADVANCED, + name = R.string.item_playaudiofiles, + actionId = R.id.action_mainFragment_to_PlayAudioFiles, + tipsId = R.string.playaudiofiles +) +public class PlayAudioFiles extends BaseFragment implements View.OnClickListener, SeekBar.OnSeekBarChangeListener { + private static final String TAG = PlayAudioFiles.class.getSimpleName(); + private EditText et_channel; + private Button mute, join, speaker, bgm, effect; + private SeekBar mixingPublishVolBar, mixingPlayoutVolBar, mixingVolBar; + private RtcEngine engine; + private int myUid; + private boolean joined = false; + private IAudioEffectManager audioEffectManager; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + handler = new Handler(); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_play_audio_files, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + et_channel = view.findViewById(R.id.et_channel); + view.findViewById(R.id.btn_join).setOnClickListener(this); + mute = view.findViewById(R.id.btn_mute); + mute.setOnClickListener(this); + speaker = view.findViewById(R.id.btn_speaker); + speaker.setOnClickListener(this); + bgm = view.findViewById(R.id.btn_bgm); + bgm.setOnClickListener(this); + effect = view.findViewById(R.id.btn_effect); + effect.setOnClickListener(this); + mixingPublishVolBar = view.findViewById(R.id.mixingPublishVolBar); + mixingPlayoutVolBar = view.findViewById(R.id.mixingPlayoutVolBar); + mixingVolBar = view.findViewById(R.id.mixingVolBar); + mixingPlayoutVolBar.setOnSeekBarChangeListener(this); + mixingPublishVolBar.setOnSeekBarChangeListener(this); + mixingVolBar.setOnSeekBarChangeListener(this); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) + { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + try + { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + String appId = getString(R.string.agora_app_id); + engine = RtcEngine.create(getContext().getApplicationContext(), appId, iRtcEngineEventHandler); + + preloadAudioEffect(); + } + catch (Exception e) + { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + /** + * To ensure smooth communication, limit the size of the audio effect file. + * We recommend using this method to preload the audio effect before calling the joinChannel method. + */ + private void preloadAudioEffect(){ + // Gets the global audio effect manager. + audioEffectManager = engine.getAudioEffectManager(); + // Preloads the audio effect (recommended). Note the file size, and preload the file before joining the channel. + // Only mp3, aac, m4a, 3gp, and wav files are supported. + // You may need to record the sound IDs and their file paths. + int id = 0; + audioEffectManager.preloadEffect(id++, Constant.EFFECT_FILE_PATH); + /** Plays an audio effect file. + * Returns + * 0: Success. + * < 0: Failure. + */ + audioEffectManager.playEffect( + 0, // The sound ID of the audio effect file to be played. + Constant.EFFECT_FILE_PATH, // The file path of the audio effect file. + -1, // The number of playback loops. -1 means an infinite loop. + 1, // pitch The pitch of the audio effect. The value ranges between 0.5 and 2. The default value is 1 (no change to the pitch). The lower the value, the lower the pitch. + 0.0, // Sets the spatial position of the effect. 0 means the effect shows ahead. + 100, // Sets the volume. The value ranges between 0 and 100. 100 is the original volume. + true // Sets whether to publish the audio effect. + ); + // Pauses all audio effects. + audioEffectManager.pauseAllEffects(); + } + + @Override + public void onDestroy() + { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if(engine != null) + { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + } + + @Override + public void onClick(View v) + { + if (v.getId() == R.id.btn_join) + { + if (!joined) + { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) + { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } + else + { + joined = false; + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + join.setText(getString(R.string.join)); + speaker.setText(getString(R.string.speaker)); + speaker.setEnabled(false); + mute.setText(getString(R.string.closemicrophone)); + mute.setEnabled(false); + bgm.setEnabled(false); + bgm.setText(getString(R.string.bgm_on)); + effect.setEnabled(false); + effect.setText(getString(R.string.effect_on)); + } + } + else if (v.getId() == R.id.btn_mute) + { + mute.setActivated(!mute.isActivated()); + mute.setText(getString(mute.isActivated() ? R.string.openmicrophone : R.string.closemicrophone)); + /**Turn off / on the microphone, stop / start local audio collection and push streaming.*/ + engine.muteLocalAudioStream(mute.isActivated()); + } + else if (v.getId() == R.id.btn_speaker) + { + speaker.setActivated(!speaker.isActivated()); + speaker.setText(getString(speaker.isActivated() ? R.string.earpiece : R.string.speaker)); + /**Turn off / on the speaker and change the audio playback route.*/ + engine.setEnableSpeakerphone(speaker.isActivated()); + } + else if(v.getId() == R.id.btn_bgm) + { + bgm.setActivated(!bgm.isActivated()); + bgm.setText(!bgm.isActivated()?getString(R.string.bgm_on):getString(R.string.bgm_off)); + if(bgm.isActivated()){ + engine.startAudioMixing(Constant.MIX_FILE_PATH, false, false, -1); + } + else{ + engine.stopAudioMixing(); + } + } + else if (v.getId() == R.id.btn_effect) + { + effect.setActivated(!effect.isActivated()); + effect.setText(!effect.isActivated() ? getString(R.string.effect_on): getString(R.string.effect_off)); + if(effect.isActivated()){ + // Resumes playing all audio effects. + audioEffectManager.resumeAllEffects(); + } + else { + // Pauses all audio effects. + audioEffectManager.pauseAllEffects(); + } + } + } + + /** + * @param channelId Specify the channel name that you want to join. + * Users that input the same channel name join the same channel.*/ + private void joinChannel(String channelId) + { + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) + { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) + { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + /**IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() + { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) + { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) + { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) + { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) + { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() + { + @Override + public void run() + { + speaker.setEnabled(true); + mute.setEnabled(true); + join.setEnabled(true); + join.setText(getString(R.string.leave)); + bgm.setEnabled(true); + effect.setEnabled(true); + } + }); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) + { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) + { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + } + }; + + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if(seekBar.getId() == R.id.mixingPublishVolBar){ + /** + * Adjusts the volume of audio mixing for publishing (sending to other users). + * @param volume: Audio mixing volume for publishing. The value ranges between 0 and 100 (default). + */ + engine.adjustAudioMixingPublishVolume(progress); + } + else if(seekBar.getId() == R.id.mixingPlayoutVolBar){ + /** + * Adjusts the volume of audio mixing for local playback. + * @param volume: Audio mixing volume for local playback. The value ranges between 0 and 100 (default). + */ + engine.adjustAudioMixingPlayoutVolume(progress); + } + else if(seekBar.getId() == R.id.mixingVolBar){ + /** + * Adjusts the volume of audio mixing. + * Call this method when you are in a channel. + * @param volume: Audio mixing volume. The value ranges between 0 and 100 (default). + */ + engine.adjustAudioMixingVolume(progress); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PreCallTest.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PreCallTest.java new file mode 100644 index 000000000..bfda0f3f8 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PreCallTest.java @@ -0,0 +1,309 @@ +package io.agora.api.example.examples.advanced; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.util.StringUtils; + +import java.util.Timer; +import java.util.TimerTask; + +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.common.model.StatisticsInfo; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.internal.LastmileProbeConfig; + +import static io.agora.api.example.common.model.Examples.ADVANCED; + +@Example( + index = 18, + group = ADVANCED, + name = R.string.item_precalltest, + actionId = R.id.action_mainFragment_to_PreCallTest, + tipsId = R.string.precalltest +) +public class PreCallTest extends BaseFragment implements View.OnClickListener { + private static final String TAG = PreCallTest.class.getSimpleName(); + + private RtcEngine engine; + private int myUid; + private Button btn_lastmile, btn_echo; + private StatisticsInfo statisticsInfo; + private TextView lastmileQuality, lastmileResult; + private static final Integer MAX_COUNT_DOWN = 8; + private int num; + private Timer timer; + private TimerTask task; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + handler = new Handler(); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_precall_test, container, false); + return view; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) { + return; + } + try { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + String appId = getString(R.string.agora_app_id); + engine = RtcEngine.create(getContext().getApplicationContext(), appId, iRtcEngineEventHandler); + } + catch (Exception e) { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + statisticsInfo = new StatisticsInfo(); + btn_echo = view.findViewById(R.id.btn_echo); + btn_echo.setOnClickListener(this); + btn_lastmile = view.findViewById(R.id.btn_lastmile); + btn_lastmile.setOnClickListener(this); + lastmileQuality = view.findViewById(R.id.lastmile_quality); + lastmileResult = view.findViewById(R.id.lastmile_result); + task = new TimerTask(){ + public void run() { + num++; + if(num >= MAX_COUNT_DOWN * 2){ + handler.post(() -> { + btn_echo.setEnabled(true); + btn_echo.setText("Start"); + }); + engine.stopEchoTest(); + timer.cancel(); + task.cancel(); + } + else if(num >= MAX_COUNT_DOWN) { + handler.post(() -> btn_echo.setText("PLaying with " + (MAX_COUNT_DOWN * 2 - num) + "Seconds")); + } + else{ + handler.post(() -> btn_echo.setText("Recording with " + (MAX_COUNT_DOWN - num) + "Seconds")); + } + } + }; + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.btn_lastmile) + { + // Configure a LastmileProbeConfig instance. + LastmileProbeConfig config = new LastmileProbeConfig(){}; + // Probe the uplink network quality. + config.probeUplink = true; + // Probe the downlink network quality. + config.probeDownlink = true; + // The expected uplink bitrate (bps). The value range is [100000, 5000000]. + config.expectedUplinkBitrate = 100000; + // The expected downlink bitrate (bps). The value range is [100000, 5000000]. + config.expectedDownlinkBitrate = 100000; + // Start the last-mile network test before joining the channel. + engine.startLastmileProbeTest(config); + btn_lastmile.setEnabled(false); + btn_lastmile.setText("Testing ..."); + } + else if (v.getId() == R.id.btn_echo){ + num = 0; + engine.startEchoTest(MAX_COUNT_DOWN); + btn_echo.setEnabled(false); + btn_echo.setText("Recording on Microphone ..."); + timer = new Timer(true); + timer.schedule(task, 1000, 1000); + } + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + } + + /** + * Implemented in the global IRtcEngineEventHandler class. + * Triggered 2 seconds after starting the last-mile test. + * @param quality + */ + @Override + public void onLastmileQuality(int quality){ + statisticsInfo.setLastMileQuality(quality); + updateLastMileResult(); + } + + /** + * Implemented in the global IRtcEngineEventHandler class. + * Triggered 30 seconds after starting the last-mile test. + * @param lastmileProbeResult + */ + @Override + public void onLastmileProbeResult(LastmileProbeResult lastmileProbeResult) { + // (1) Stop the test. Agora recommends not calling any other API method before the test ends. + engine.stopLastmileProbeTest(); + statisticsInfo.setLastMileProbeResult(lastmileProbeResult); + updateLastMileResult(); + handler.post(() -> { + btn_lastmile.setEnabled(true); + btn_lastmile.setText("Start"); + }); + } + }; + + private void updateLastMileResult() { + handler.post(() -> { + if(statisticsInfo.getLastMileQuality() != null){ + lastmileQuality.setText("Quality: " + statisticsInfo.getLastMileQuality()); + } + if(statisticsInfo.getLastMileResult() != null){ + lastmileResult.setText(statisticsInfo.getLastMileResult()); + } + }); + } + +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ProcessAudioRawData.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ProcessAudioRawData.java new file mode 100755 index 000000000..6bf6d3e67 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ProcessAudioRawData.java @@ -0,0 +1,507 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.media.AudioFormat; +import android.media.AudioManager; +import android.os.Bundle; +import android.os.Handler; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.Switch; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.examples.advanced.customaudio.AudioPlayer; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IAudioFrameObserver; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.models.ChannelMediaOptions; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.api.example.common.model.Examples.BASIC; + +/** + * This demo demonstrates how to make a one-to-one voice call + * + * @author cjw + */ +@Example( + index = 24, + group = ADVANCED, + name = R.string.item_raw_audio, + actionId = R.id.action_mainFragment_raw_audio, + tipsId = R.string.rawaudio +) +public class ProcessAudioRawData extends BaseFragment implements View.OnClickListener, CompoundButton.OnCheckedChangeListener { + private static final String TAG = ProcessAudioRawData.class.getSimpleName(); + private EditText et_channel; + private Button mute, join, speaker; + private Switch loopback; + private RtcEngine engine; + private int myUid; + private boolean joined = false; + private boolean isEnableLoopBack = false; + private AudioPlayer mAudioPlayer; + private static final Integer SAMPLE_RATE = 44100; + private static final Integer SAMPLE_NUM_OF_CHANNEL = 1; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + handler = new Handler(); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_raw_audio, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + et_channel = view.findViewById(R.id.et_channel); + view.findViewById(R.id.btn_join).setOnClickListener(this); + mute = view.findViewById(R.id.btn_mute); + mute.setOnClickListener(this); + speaker = view.findViewById(R.id.btn_speaker); + speaker.setOnClickListener(this); + loopback = view.findViewById(R.id.loopback); + loopback.setOnCheckedChangeListener(this); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) { + return; + } + try { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + String appId = getString(R.string.agora_app_id); + engine = RtcEngine.create(getContext().getApplicationContext(), appId, iRtcEngineEventHandler); + /** Registers the audio observer object. + * + * @param observer Audio observer object to be registered. See {@link IAudioFrameObserver IAudioFrameObserver}. Set the value as @p null to cancel registering, if necessary. + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.registerAudioFrameObserver(new IAudioFrameObserver() { + /** Occurs when the recorded audio frame is received. + * + * @param samples Sample data of the frame. + * @param numOfSamples Number of samples. + * @param bytesPerSample Number of bytes per audio sample. For example, each PCM audio sample usually takes up 16 bits (2 bytes). + * @param channels Number of audio channels. If the channel uses stereo, the data is interleaved. + *

+ * @param samplesPerSec The number of samples per channel per second in the audio frame. + * @return + * + */ + @Override + public boolean onRecordFrame(byte[] samples, int numOfSamples, int bytesPerSample, int channels, int samplesPerSec) { + if(isEnableLoopBack){ + mAudioPlayer.play(samples, 0, numOfSamples * bytesPerSample); + } + return false; + } + /** Occurs when the playback audio frame is received. + * + * @param samples Sample data of the frame. + * @param numOfSamples Number of samples. + * @param bytesPerSample Number of bytes per audio sample. For example, each PCM audio sample usually takes up 16 bits (2 bytes). + * @param channels Number of audio channels. If the channel uses stereo, the data is interleaved. + * + * @param samplesPerSec The number of samples per channel per second in the audio frame. + * @return + * + */ + @Override + public boolean onPlaybackFrame(byte[] samples, int numOfSamples, int bytesPerSample, int channels, int samplesPerSec) { + return false; + } + + /** Occurs when the audio frame of a specified user before mixing. + * + * @note This callback only returns the single-channel data. + * + * @param samples Sample data of the frame. + * @param numOfSamples Number of samples. + * @param bytesPerSample Number of bytes per audio sample. For example, each PCM audio sample usually takes up 16 bits (2 bytes). + * @param channels Number of audio channels. If the channel uses stereo, the data is interleaved. + * + * @param samplesPerSec The number of samples per channel per second in the audio frame. + * @param uid The User ID. + * @return + * + */ + @Override + public boolean onPlaybackFrameBeforeMixing(byte[] samples, int numOfSamples, int bytesPerSample, int channels, int samplesPerSec, int uid) { + return false; + } + + /** Occurs when the mixed recorded and playback audio frame. + * + * @param samples Sample data of the frame. + * @param numOfSamples Number of samples. + * @param bytesPerSample Number of bytes per audio sample. For example, each PCM audio sample usually takes up 16 bits (2 bytes). + * @param channels Number of audio channels. If the channel uses stereo, the data is interleaved. + * + * @param samplesPerSec The number of samples per channel per second in the audio frame. + * @return + * + */ + @Override + public boolean onMixedFrame(byte[] samples, int numOfSamples, int bytesPerSample, int channels, int samplesPerSec) { + return false; + } + + /** + * + * @return + * + */ + @Override + public boolean isMultipleChannelFrameWanted() { + return false; + } + + /** + * Occurs when the playback audio frame is received. + * @param samples Sample data of the frame. + * @param numOfSamples Number of samples. + * @param bytesPerSample Number of bytes per audio sample. For example, each PCM audio sample usually takes up 16 bits (2 bytes). + * @param channels Number of audio channels. If the channel uses stereo, the data is interleaved. + * @param samplesPerSec The number of samples per channel per second in the audio frame. + * @param uid The User ID. + * @param channelId The Channel ID. + * @return + * + */ + @Override + public boolean onPlaybackFrameBeforeMixingEx(byte[] samples, int numOfSamples, int bytesPerSample, int channels, int samplesPerSec, int uid, String channelId) { + return false; + } + }); + mAudioPlayer = new AudioPlayer(AudioManager.STREAM_VOICE_CALL, SAMPLE_RATE, SAMPLE_NUM_OF_CHANNEL, AudioFormat.CHANNEL_OUT_MONO); + } + catch (Exception e) { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if (engine != null) { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + mAudioPlayer.stopPlayer(); + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.btn_join) { + if (!joined) { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } else { + joined = false; + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + join.setText(getString(R.string.join)); + speaker.setText(getString(R.string.speaker)); + speaker.setEnabled(false); + mute.setText(getString(R.string.closemicrophone)); + mute.setEnabled(false); + } + } else if (v.getId() == R.id.btn_mute) { + mute.setActivated(!mute.isActivated()); + mute.setText(getString(mute.isActivated() ? R.string.openmicrophone : R.string.closemicrophone)); + /**Turn off / on the microphone, stop / start local audio collection and push streaming.*/ + engine.muteLocalAudioStream(mute.isActivated()); + } else if (v.getId() == R.id.btn_speaker) { + speaker.setActivated(!speaker.isActivated()); + speaker.setText(getString(speaker.isActivated() ? R.string.earpiece : R.string.speaker)); + /**Turn off / on the speaker and change the audio playback route.*/ + engine.setEnableSpeakerphone(speaker.isActivated()); + } + } + + /** + * @param channelId Specify the channel name that you want to join. + * Users that input the same channel name join the same channel. + */ + private void joinChannel(String channelId) { + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + engine.enableAudioVolumeIndication(1000, 3, true); + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + + + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + mAudioPlayer.startPlayer(); + myUid = uid; + joined = true; + handler.post(new Runnable() { + @Override + public void run() { + speaker.setEnabled(true); + mute.setEnabled(true); + join.setEnabled(true); + join.setText(getString(R.string.leave)); + loopback.setEnabled(true); + } + }); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + } + + @Override + public void onActiveSpeaker(int uid) { + super.onActiveSpeaker(uid); + Log.i(TAG, String.format("onActiveSpeaker:%d", uid)); + } + }; + + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean b) { + isEnableLoopBack = b; + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ProcessRawData.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ProcessRawData.java new file mode 100644 index 000000000..9c64c5817 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/ProcessRawData.java @@ -0,0 +1,501 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import io.agora.advancedvideo.rawdata.MediaDataAudioObserver; +import io.agora.advancedvideo.rawdata.MediaDataObserverPlugin; +import io.agora.advancedvideo.rawdata.MediaDataVideoObserver; +import io.agora.advancedvideo.rawdata.MediaPreProcessing; +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.api.example.utils.YUVUtils; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.rtc.Constants.RAW_AUDIO_FRAME_OP_MODE_READ_ONLY; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; +import static io.agora.rtc.video.VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15; +import static io.agora.rtc.video.VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; +import static io.agora.rtc.video.VideoEncoderConfiguration.STANDARD_BITRATE; +import static io.agora.rtc.video.VideoEncoderConfiguration.VD_640x360; + +@Example( + index = 10, + group = ADVANCED, + name = R.string.item_processraw, + actionId = R.id.action_mainFragment_to_ProcessRawData, + tipsId = R.string.processrawdata +) +public class ProcessRawData extends BaseFragment implements View.OnClickListener, MediaDataVideoObserver, + MediaDataAudioObserver { + private static final String TAG = ProcessRawData.class.getSimpleName(); + + private FrameLayout fl_local, fl_remote; + private Button join, blurBtn; + private EditText et_channel; + private RtcEngine engine; + private int myUid; + private boolean joined = false, blur = true; + private MediaDataObserverPlugin mediaDataObserverPlugin; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) { + return; + } + try { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); + } + catch (Exception e) { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_process_rawdata, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + blurBtn = view.findViewById(R.id.btn_blur); + et_channel = view.findViewById(R.id.et_channel); + join.setOnClickListener(this); + blurBtn.setOnClickListener(this); + fl_local = view.findViewById(R.id.fl_local); + fl_remote = view.findViewById(R.id.fl_remote); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + mediaDataObserverPlugin = MediaDataObserverPlugin.the(); + MediaPreProcessing.setCallback(mediaDataObserverPlugin); + MediaPreProcessing.setVideoCaptureByteBuffer(mediaDataObserverPlugin.byteBufferCapture); + mediaDataObserverPlugin.addVideoObserver(this); + } + + @Override + public void onDestroy() { + if (mediaDataObserverPlugin != null) { + mediaDataObserverPlugin.removeVideoObserver(this); + mediaDataObserverPlugin.removeAllBuffer(); + } + MediaPreProcessing.releasePoint(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if (engine != null) { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + super.onDestroy(); + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.btn_join) { + if (!joined) { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE, + Permission.Group.CAMERA + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } else { + joined = false; + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + join.setText(getString(R.string.join)); + } + } else if (v.getId() == R.id.btn_blur) { + if (!blur) { + blur = true; + blurBtn.setText(getString(R.string.blur)); + } else { + blur = false; + blurBtn.setText(getString(R.string.closeblur)); + } + } + } + + private void joinChannel(String channelId) { + // Check if the context is valid + Context context = getContext(); + if (context == null) { + return; + } + + // Create render view by RtcEngine + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + // Add to the local container + fl_local.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup local video to render your local camera preview + engine.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0)); + + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + // Enable video module + engine.enableVideo(); + // Setup video encoding configs + engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + ((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(), + VideoEncoderConfiguration.FRAME_RATE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate()), + STANDARD_BITRATE, + VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) + )); + /**Set up to play remote sound with receiver*/ + engine.setDefaultAudioRoutetoSpeakerphone(false); + engine.setEnableSpeakerphone(false); + + /** + * Sets the audio recording format for the onRecordAudioFrame callback. + * sampleRate Sets the sample rate (samplesPerSec) returned in the onRecordAudioFrame callback, which can be set as 8000, 16000, 32000, 44100, or 48000 Hz. + * channel Sets the number of audio channels (channels) returned in the onRecordAudioFrame callback: + * 1: Mono + * 2: Stereo + * mode Sets the use mode (see RAW_AUDIO_FRAME_OP_MODE_TYPE) of the onRecordAudioFrame callback. + * samplesPerCall Sets the number of samples returned in the onRecordAudioFrame callback. samplesPerCall is usually set as 1024 for RTMP streaming. + * The SDK triggers the onRecordAudioFrame callback according to the sample interval. Ensure that the sample interval ≥ 0.01 (s). And, Sample interval (sec) = samplePerCall/(sampleRate × channel). + */ + engine.setRecordingAudioFrameParameters(4000, 1, RAW_AUDIO_FRAME_OP_MODE_READ_ONLY, 1024); + + /** + * Sets the audio playback format for the onPlaybackAudioFrame callback. + * sampleRate Sets the sample rate (samplesPerSec) returned in the onRecordAudioFrame callback, which can be set as 8000, 16000, 32000, 44100, or 48000 Hz. + * channel Sets the number of audio channels (channels) returned in the onRecordAudioFrame callback: + * 1: Mono + * 2: Stereo + * mode Sets the use mode (see RAW_AUDIO_FRAME_OP_MODE_TYPE) of the onRecordAudioFrame callback. + * samplesPerCall Sets the number of samples returned in the onRecordAudioFrame callback. samplesPerCall is usually set as 1024 for RTMP streaming. + * The SDK triggers the onRecordAudioFrame callback according to the sample interval. Ensure that the sample interval ≥ 0.01 (s). And, Sample interval (sec) = samplePerCall/(sampleRate × channel). + */ + engine.setPlaybackAudioFrameParameters(4000, 1, RAW_AUDIO_FRAME_OP_MODE_READ_ONLY, 1024); + + /** + * Sets the mixed audio format for the onMixedAudioFrame callback. + * sampleRate Sets the sample rate (samplesPerSec) returned in the onMixedAudioFrame callback, which can be set as 8000, 16000, 32000, 44100, or 48000 Hz. + * samplesPerCall Sets the number of samples (samples) returned in the onMixedAudioFrame callback. samplesPerCall is usually set as 1024 for RTMP streaming. + */ + engine.setMixedAudioFrameParameters(8000, 1024); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() { + @Override + public void run() { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + } + }); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + handler.post(() -> + { + if (mediaDataObserverPlugin != null) { + mediaDataObserverPlugin.addDecodeBuffer(uid); + } + + /**Display remote video stream*/ + // Create render view by RtcEngine + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + surfaceView.setZOrderMediaOverlay(true); + if (fl_remote.getChildCount() > 0) { + fl_remote.removeAllViews(); + } + // Add to the remote container + fl_remote.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup remote video to render + engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, uid)); + }); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + handler.post(new Runnable() { + @Override + public void run() { + if (mediaDataObserverPlugin != null) { + mediaDataObserverPlugin.removeDecodeBuffer(uid); + } + /**Clear render view + Note: The video will stay at its last frame, to completely remove it you will need to + remove the SurfaceView from its parent*/ + engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); + } + }); + } + }; + + @Override + public void onCaptureVideoFrame(byte[] data, int frameType, int width, int height, int bufferLength, int yStride, int uStride, int vStride, int rotation, long renderTimeMs) { + /**You can do some processing on the video frame here*/ + Log.e(TAG, "onCaptureVideoFrame0"); + if (blur) { + return; + } + Bitmap bitmap = YUVUtils.i420ToBitmap(width, height, rotation, bufferLength, data, yStride, uStride, vStride); + Bitmap bmp = YUVUtils.blur(getContext(), bitmap, 4); + System.arraycopy(YUVUtils.bitmapToI420(width, height, bmp), 0, data, 0, bufferLength); + } + + @Override + public void onRenderVideoFrame(int uid, byte[] data, int frameType, int width, int height, int bufferLength, int yStride, int uStride, int vStride, int rotation, long renderTimeMs) { + if (blur) { + return; + } + Bitmap bmp = YUVUtils.blur(getContext(), YUVUtils.i420ToBitmap(width, height, rotation, bufferLength, data, yStride, uStride, vStride), 4); + System.arraycopy(YUVUtils.bitmapToI420(width, height, bmp), 0, data, 0, bufferLength); + } + + @Override + public void onPreEncodeVideoFrame(byte[] data, int frameType, int width, int height, int bufferLength, int yStride, int uStride, int vStride, int rotation, long renderTimeMs) { + /**You can do some processing on the video frame here*/ + Log.e(TAG, "onPreEncodeVideoFrame0"); + } + + /** + * Retrieves the recorded audio frame. + * @param audioFrameType only support FRAME_TYPE_PCM16 + * @param samples The number of samples per channel in the audio frame. + * @param bytesPerSample The number of bytes per audio sample, which is usually 16-bit (2-byte). + * @param channels The number of audio channels. + * 1: Mono + * 2: Stereo (the data is interleaved) + * @param samplesPerSec The sample rate. + * @param renderTimeMs The timestamp of the external audio frame. + * @param bufferLength audio frame size*/ + @Override + public void onRecordAudioFrame(byte[] data, int audioFrameType, int samples, int bytesPerSample, int channels, int samplesPerSec, long renderTimeMs, int bufferLength) { + + } + + /** + * Retrieves the audio playback frame for getting the audio. + * @param audioFrameType only support FRAME_TYPE_PCM16 + * @param samples The number of samples per channel in the audio frame. + * @param bytesPerSample The number of bytes per audio sample, which is usually 16-bit (2-byte). + * @param channels The number of audio channels. + * 1: Mono + * 2: Stereo (the data is interleaved) + * @param samplesPerSec The sample rate. + * @param renderTimeMs The timestamp of the external audio frame. + * @param bufferLength audio frame size*/ + @Override + public void onPlaybackAudioFrame(byte[] data, int audioFrameType, int samples, int bytesPerSample, int channels, int samplesPerSec, long renderTimeMs, int bufferLength) { + + } + + + /** + * Retrieves the audio frame of a specified user before mixing. + * The SDK triggers this callback if isMultipleChannelFrameWanted returns false. + * @param uid remote user id + * @param audioFrameType only support FRAME_TYPE_PCM16 + * @param samples The number of samples per channel in the audio frame. + * @param bytesPerSample The number of bytes per audio sample, which is usually 16-bit (2-byte). + * @param channels The number of audio channels. + * 1: Mono + * 2: Stereo (the data is interleaved) + * @param samplesPerSec The sample rate. + * @param renderTimeMs The timestamp of the external audio frame. + * @param bufferLength audio frame size*/ + @Override + public void onPlaybackAudioFrameBeforeMixing(int uid, byte[] data, int audioFrameType, int samples, int bytesPerSample, int channels, int samplesPerSec, long renderTimeMs, int bufferLength) { + + } + + /** + * Retrieves the mixed recorded and playback audio frame. + * @param audioFrameType only support FRAME_TYPE_PCM16 + * @param samples The number of samples per channel in the audio frame. + * @param bytesPerSample The number of bytes per audio sample, which is usually 16-bit (2-byte). + * @param channels The number of audio channels. + * 1: Mono + * 2: Stereo (the data is interleaved) + * @param samplesPerSec The sample rate. + * @param renderTimeMs The timestamp of the external audio frame. + * @param bufferLength audio frame size*/ + @Override + public void onMixedAudioFrame(byte[] data, int audioFrameType, int samples, int bytesPerSample, int channels, int samplesPerSec, long renderTimeMs, int bufferLength) { + + } + +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PushExternalVideo.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PushExternalVideo.java new file mode 100644 index 000000000..9a4658f57 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/PushExternalVideo.java @@ -0,0 +1,516 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.opengl.EGLSurface; +import android.opengl.GLES11Ext; +import android.opengl.GLES20; +import android.opengl.Matrix; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import java.io.IOException; + +import io.agora.api.component.gles.ProgramTextureOES; +import io.agora.api.component.gles.core.EglCore; +import io.agora.api.component.gles.core.GlUtil; +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.gl.VideoFrame; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.video.AgoraVideoFrame; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; +import static io.agora.rtc.video.VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15; +import static io.agora.rtc.video.VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; +import static io.agora.rtc.video.VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_FIXED_PORTRAIT; +import static io.agora.rtc.video.VideoEncoderConfiguration.STANDARD_BITRATE; +import static io.agora.rtc.video.VideoEncoderConfiguration.VD_640x360; + +@Example( + index = 5, + group = ADVANCED, + name = R.string.item_pushexternal, + actionId = R.id.action_mainFragment_to_PushExternalVideo, + tipsId = R.string.pushexternalvideo +) +public class PushExternalVideo extends BaseFragment implements View.OnClickListener, TextureView.SurfaceTextureListener, + SurfaceTexture.OnFrameAvailableListener { + private static final String TAG = PushExternalVideo.class.getSimpleName(); + private final int DEFAULT_CAPTURE_WIDTH = 640; + private final int DEFAULT_CAPTURE_HEIGHT = 480; + + private FrameLayout fl_local, fl_remote; + private Button join; + private EditText et_channel; + private RtcEngine engine; + private int myUid; + private volatile boolean joined = false; + + private int mPreviewTexture; + private SurfaceTexture mPreviewSurfaceTexture; + private EglCore mEglCore; + private EGLSurface mDummySurface; + private EGLSurface mDrawSurface; + private ProgramTextureOES mProgram; + private float[] mTransform = new float[16]; + private float[] mMVPMatrix = new float[16]; + private boolean mMVPMatrixInit = false; + private Camera mCamera; + private int mFacing = Camera.CameraInfo.CAMERA_FACING_FRONT; + private boolean mPreviewing = false; + private int mSurfaceWidth; + private int mSurfaceHeight; + private boolean mTextureDestroyed; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_push_externalvideo, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + et_channel = view.findViewById(R.id.et_channel); + view.findViewById(R.id.btn_join).setOnClickListener(this); + fl_local = view.findViewById(R.id.fl_local); + fl_remote = view.findViewById(R.id.fl_remote); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) { + return; + } + try { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); + } + catch (Exception e) { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + + @Override + public void onDestroy() { + /**leaveChannel and Destroy the RtcEngine instance*/ + if (engine != null) { + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + super.onDestroy(); + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.btn_join) { + if (!joined) { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE, + Permission.Group.CAMERA + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } else { + fl_local.setVisibility(View.GONE); + getActivity().onBackPressed(); + } + } + } + + private void joinChannel(String channelId) { +// engine.setParameters("{\"rtc.log_filter\":65535}"); + // Check if the context is valid + Context context = getContext(); + if (context == null) { + return; + } + + // Create render view by RtcEngine + TextureView textureView = new TextureView(getContext()); + //add SurfaceTextureListener + textureView.setSurfaceTextureListener(this); + // Add to the local container + fl_local.addView(textureView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + /**Set up to play remote sound with receiver*/ + engine.setDefaultAudioRoutetoSpeakerphone(false); + engine.setEnableSpeakerphone(false); + + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + // Enables the video module. + engine.enableVideo(); + // Setup video encoding configs + engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + ((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(), + VideoEncoderConfiguration.FRAME_RATE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate()), + STANDARD_BITRATE, + VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) + )); + /**Configures the external video source. + * @param enable Sets whether or not to use the external video source: + * true: Use the external video source. + * false: Do not use the external video source. + * @param useTexture Sets whether or not to use texture as an input: + * true: Use texture as an input. + * false: (Default) Do not use texture as an input. + * @param pushMode Sets whether or not the external video source needs to call the PushExternalVideoFrame + * method to send the video frame to the Agora SDK: + * true: Use the push mode. + * false: Use the pull mode (not supported).*/ + engine.setExternalVideoSource(true, true, true); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + if (mTextureDestroyed) { + return; + } + + if (!mEglCore.isCurrent(mDrawSurface)) { + mEglCore.makeCurrent(mDrawSurface); + } + try { + mPreviewSurfaceTexture.updateTexImage(); + mPreviewSurfaceTexture.getTransformMatrix(mTransform); + } + catch (Exception e) { + e.printStackTrace(); + } + + /**The rectangle ratio of frames and the screen surface may be different, so cropping may + * happen when display frames to the screen. + * The display transformation matrix does not change for the same camera when the screen + * orientation remains the same.*/ + if (!mMVPMatrixInit) { + /***/ + /**For simplicity, we only consider the activity as portrait mode. In this case, the captured + * images should be rotated 90 degrees (left or right).Thus the frame width and height + * should be swapped.*/ + float frameRatio = DEFAULT_CAPTURE_HEIGHT / (float) DEFAULT_CAPTURE_WIDTH; + float surfaceRatio = mSurfaceWidth / (float) mSurfaceHeight; + Matrix.setIdentityM(mMVPMatrix, 0); + + if (frameRatio >= surfaceRatio) { + float w = DEFAULT_CAPTURE_WIDTH * surfaceRatio; + float scaleW = DEFAULT_CAPTURE_HEIGHT / w; + Matrix.scaleM(mMVPMatrix, 0, scaleW, 1, 1); + } else { + float h = DEFAULT_CAPTURE_HEIGHT / surfaceRatio; + float scaleH = DEFAULT_CAPTURE_WIDTH / h; + Matrix.scaleM(mMVPMatrix, 0, 1, scaleH, 1); + } + mMVPMatrixInit = true; + } + GLES20.glViewport(0, 0, mSurfaceWidth, mSurfaceHeight); + mProgram.drawFrame(mPreviewTexture, mTransform, mMVPMatrix); + mEglCore.swapBuffers(mDrawSurface); + + if (joined) { + /**about AgoraVideoFrame, see https://docs.agora.io/en/Video/API%20Reference/java/classio_1_1agora_1_1rtc_1_1video_1_1_agora_video_frame.html*/ + AgoraVideoFrame frame = new AgoraVideoFrame(); + frame.textureID = mPreviewTexture; + frame.format = AgoraVideoFrame.FORMAT_TEXTURE_OES; + frame.transform = mTransform; + frame.stride = DEFAULT_CAPTURE_HEIGHT; + frame.height = DEFAULT_CAPTURE_WIDTH; + frame.eglContext14 = mEglCore.getEGLContext(); + frame.timeStamp = System.currentTimeMillis(); + /**Pushes the video frame using the AgoraVideoFrame class and passes the video frame to the Agora SDK. + * Call the setExternalVideoSource method and set pushMode as true before calling this + * method. Otherwise, a failure returns after calling this method. + * @param frame AgoraVideoFrame + * @return + * true: The frame is pushed successfully. + * false: Failed to push the frame. + * PS: + * In the Communication profile, the SDK does not support textured video frames.*/ + boolean a = engine.pushExternalVideoFrame(frame); + Log.e(TAG, "pushExternalVideoFrame:" + a); + } + } + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + Log.i(TAG, "onSurfaceTextureAvailable"); + mTextureDestroyed = false; + mSurfaceWidth = width; + mSurfaceHeight = height; + mEglCore = new EglCore(); + mDummySurface = mEglCore.createOffscreenSurface(1, 1); + mEglCore.makeCurrent(mDummySurface); + mPreviewTexture = GlUtil.createTextureObject(GLES11Ext.GL_TEXTURE_EXTERNAL_OES); + mPreviewSurfaceTexture = new SurfaceTexture(mPreviewTexture); + mPreviewSurfaceTexture.setOnFrameAvailableListener(this); + mDrawSurface = mEglCore.createWindowSurface(surface); + mProgram = new ProgramTextureOES(); + if (mCamera != null || mPreviewing) { + Log.e(TAG, "Camera preview has been started"); + return; + } + try { + mCamera = Camera.open(mFacing); + /**It is assumed to capture images of resolution 640x480. During development, it should + * be the most suitable supported resolution that best fits the scenario.*/ + Camera.Parameters parameters = mCamera.getParameters(); + parameters.setPreviewSize(DEFAULT_CAPTURE_WIDTH, DEFAULT_CAPTURE_HEIGHT); + mCamera.setParameters(parameters); + mCamera.setPreviewTexture(mPreviewSurfaceTexture); + /**The display orientation is 90 for both front and back facing cameras using a surface + * texture for the preview when the screen is in portrait mode.*/ + mCamera.setDisplayOrientation(90); + mCamera.startPreview(); + mPreviewing = true; + } + catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + Log.i(TAG, "onSurfaceTextureDestroyed"); + mTextureDestroyed = true; + if (mCamera != null && mPreviewing) { + mCamera.stopPreview(); + mPreviewing = false; + mCamera.release(); + mCamera = null; + } + mProgram.release(); + mEglCore.releaseSurface(mDummySurface); + mEglCore.releaseSurface(mDrawSurface); + mEglCore.release(); + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() { + @Override + public void run() { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + } + }); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + handler.post(() -> + { + /**Display remote video stream*/ + // Create render view by RtcEngine + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + surfaceView.setZOrderMediaOverlay(true); + if (fl_remote.getChildCount() > 0) { + fl_remote.removeAllViews(); + } + // Add to the remote container + fl_remote.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup remote video to render + engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, uid)); + }); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + handler.post(new Runnable() { + @Override + public void run() { + /**Clear render view + Note: The video will stay at its last frame, to completely remove it you will need to + remove the SurfaceView from its parent*/ + engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); + } + }); + } + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/RTMPInjection.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/RTMPInjection.java new file mode 100644 index 000000000..cbe4793f3 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/RTMPInjection.java @@ -0,0 +1,554 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.live.LiveInjectStreamConfig; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; +import static io.agora.rtc.video.VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15; +import static io.agora.rtc.video.VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; +import static io.agora.rtc.video.VideoEncoderConfiguration.STANDARD_BITRATE; +import static io.agora.rtc.video.VideoEncoderConfiguration.VD_640x360; + +/** + * This example demonstrates how to pull flow from an external address. + * + * Important: + * Users who push and pull streams cannot be in one channel, + * otherwise unexpected errors will occur. + */ +@Example( + index = 4, + group = ADVANCED, + name = R.string.item_rtmpinjection, + actionId = R.id.action_mainFragment_to_RTMPInjection, + tipsId = R.string.rtmpinjection +) +public class RTMPInjection extends BaseFragment implements View.OnClickListener +{ + private static final String TAG = RTMPInjection.class.getSimpleName(); + + private FrameLayout fl_local, fl_remote; + private EditText et_url, et_channel; + private Button join, inject; + private RtcEngine engine; + private int myUid; + private boolean joined = false, injecting = false; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_rtmp_injection, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); + fl_local = view.findViewById(R.id.fl_local); + fl_remote = view.findViewById(R.id.fl_remote); + et_channel = view.findViewById(R.id.et_channel); + et_url = view.findViewById(R.id.et_url); + join = view.findViewById(R.id.btn_join); + join.setOnClickListener(this); + inject = view.findViewById(R.id.btn_inject); + inject.setOnClickListener(this); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) + { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + try + { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); + } + catch (Exception e) + { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() + { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if(engine != null) + { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + } + + @Override + public void onClick(View v) + { + + if (v.getId() == R.id.btn_join) + { + if(!joined) + { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) + { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE, + Permission.Group.CAMERA + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } + else + { + engine.leaveChannel(); + joined = false; + join.setText(getString(R.string.join)); + injecting = false; + inject.setEnabled(false); + inject.setText(getString(R.string.inject)); + } + } + else if (v.getId() == R.id.btn_inject) + { + /**Ensure that the user joins a channel before calling this method.*/ + if(joined && !injecting) + { + startInjection(); + } + else if(joined && injecting) + { + stopInjection(); + } + } + } + + private void joinChannel(String channelId) + { + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + + // Create render view by RtcEngine + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + // Add to the local container + fl_local.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup local video to render your local camera preview + engine.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0)); + /**Set up to play remote sound with receiver*/ + engine.setDefaultAudioRoutetoSpeakerphone(false); + engine.setEnableSpeakerphone(false); + + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + // Enable video module + engine.enableVideo(); + // Setup video encoding configs + engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + ((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(), + VideoEncoderConfiguration.FRAME_RATE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate()), + STANDARD_BITRATE, + VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) + )); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) + { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) + { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + private void startInjection() + { + /**Configuration of the imported live broadcast voice or video stream. + * See */ + LiveInjectStreamConfig config = new LiveInjectStreamConfig(); + /**Injects an online media stream to a live broadcast. + * If this method call is successful, the server pulls the voice or video stream and injects + * it into a live channel. This is applicable to scenarios where all audience members in the + * channel can watch a live show and interact with each other. + * The addInjectStreamUrl method call triggers the following callbacks: + * The local client: + * onStreamInjectedStatus, with the state of the injecting the online stream. + * onUserJoined(uid: 666), if the method call is successful and the online media stream + * is injected into the channel. + * The remote client: + * onUserJoined(uid: 666), if the method call is successful and the online media stream + * is injected into the channel. + * @param url The URL address to be added to the ongoing live broadcast. Valid protocols are RTMP, HLS, and HTTP-FLV. + * Supported FLV audio codec type: AAC. + * Supported FLV video codec type: H264(AVC). + * @param config The LiveInjectStreamConfig object which contains the configuration information + * for the added voice or video stream. + * @return + * 0: Success. + * < 0: Failure. + * ERR_INVALID_ARGUMENT(2): The injected URL does not exist. Call this method again to + * inject the stream and ensure that the URL is valid. + * ERR_NOT_READY(3): The user is not in the channel. + * ERR_NOT_SUPPORTED(4): The channel profile is not Live Broadcast. Call the setChannelProfile + * method and set the channel profile to Live Broadcast before calling this method. + * ERR_NOT_INITIALIZED(7): The SDK is not initialized. Ensure that the RtcEngine object + * is initialized before using this method. + * PS: + * This method applies to the Live-Broadcast profile only. + * Ensure that you enable the RTMP Converter service before using this function. See + * Prerequisites in Push Streams to CDN. + * You can inject only one media stream into the channel at the same time.*/ + engine.addInjectStreamUrl(et_url.getText().toString(), config); + } + + private void stopInjection() + { + injecting = false; + inject.setEnabled(true); + inject.setText(getString(R.string.inject)); + /**Removes the injected online media stream from a live broadcast. + * This method removes the URL address (added by addInjectStreamUrl) from a live broadcast. + * If this method call is successful, the SDK triggers the onUserOffline callback and returns + * a stream uid of 666. + * @param url HTTP/HTTPS URL address of the added stream to be removed. + * @return + * 0: Success. + * < 0: Failure.*/ + int ret = engine.removeInjectStreamUrl(et_url.getText().toString()); + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() + { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) + { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) + { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) + { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) + { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() + { + @Override + public void run() + { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + inject.setEnabled(true); + inject.setText(getString(R.string.inject)); + } + }); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Since v2.9.0. + * Occurs when the remote video state changes. + * PS: This callback does not work properly when the number of users (in the Communication + * profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the remote user whose video state changes. + * @param state State of the remote video: + * REMOTE_VIDEO_STATE_STOPPED(0): The remote video is in the default state, probably due + * to REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3), REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5), + * or REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7). + * REMOTE_VIDEO_STATE_STARTING(1): The first remote video packet is received. + * REMOTE_VIDEO_STATE_DECODING(2): The remote video stream is decoded and plays normally, + * probably due to REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY (2), + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4), REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6), + * or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9). + * REMOTE_VIDEO_STATE_FROZEN(3): The remote video is frozen, probably due to + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1) or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8). + * REMOTE_VIDEO_STATE_FAILED(4): The remote video fails to start, probably due to + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0). + * @param reason The reason of the remote video state change: + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0): Internal reasons. + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3): The local user stops receiving the remote + * video stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote + * video stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5): The remote user stops sending the video + * stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the video + * stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8): The remote media stream falls back to the + * audio-only stream due to poor network conditions. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9): The remote media stream switches + * back to the video stream after the network conditions improve. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method until + * the SDK triggers this callback.*/ + @Override + public void onRemoteVideoStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteVideoStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteVideoStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Reports the status of injecting the online media stream. + * @param url The URL address of the externally injected stream. + * @param uid User ID. + * @param status + * INJECT_STREAM_STATUS_START_SUCCESS(0): The external video stream imports successfully. + * INJECT_STREAM_STATUS_START_ALREADY_EXIST(1): The external video stream already exists. + * INJECT_STREAM_STATUS_START_UNAUTHORIZED(2): The external video stream import is unauthorized. + * INJECT_STREAM_STATUS_START_TIMEDOUT(3): Timeout when importing the external video stream. + * INJECT_STREAM_STATUS_START_FAILED(4): The external video stream fails to import. + * INJECT_STREAM_STATUS_STOP_SUCCESS(5): The external video stream stops importing successfully. + * INJECT_STREAM_STATUS_STOP_NOT_FOUND(6): No external video stream is found. + * INJECT_STREAM_STATUS_STOP_UNAUTHORIZED(7): The external video stream stops from being unauthorized. + * INJECT_STREAM_STATUS_STOP_TIMEDOUT(8): Timeout when stopping the import of the external video stream. + * INJECT_STREAM_STATUS_STOP_FAILED(9): Fails to stop importing the external video stream. + * INJECT_STREAM_STATUS_BROKEN(10): The external video stream import is interrupted.*/ + @Override + public void onStreamInjectedStatus(String url, int uid, int status) + { + super.onStreamInjectedStatus(url, uid, status); + Log.i(TAG, "onStreamInjectedStatus->" + url + ", uid->" + uid + ", status->" + status); + if(status == Constants.INJECT_STREAM_STATUS_START_SUCCESS) + { + /**After confirming the successful push, make changes to the UI.*/ + injecting = true; + handler.post(new Runnable() + { + @Override + public void run() + { + inject.setEnabled(true); + inject.setText(getString(R.string.stopinject)); + } + }); + } + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) + { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + handler.post(() -> + { + + /**Display remote video stream*/ + SurfaceView surfaceView = null; + // Create render view by RtcEngine + surfaceView = RtcEngine.CreateRendererView(context); + surfaceView.setZOrderMediaOverlay(true); + if(fl_remote.getChildCount() > 0) + { + fl_remote.removeAllViews(); + } + // Add to the remote container + fl_remote.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + // Setup remote video to render + engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, uid)); + }); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) + { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + handler.post(new Runnable() { + @Override + public void run() { + /**Clear render view + Note: The video will stay at its last frame, to completely remove it you will need to + remove the SurfaceView from its parent*/ + engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); + } + }); + } + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/RTMPStreaming.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/RTMPStreaming.java new file mode 100644 index 000000000..0abe1a5c4 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/RTMPStreaming.java @@ -0,0 +1,630 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.Switch; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import io.agora.api.component.Constant; +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.live.LiveTranscoding; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.video.AgoraImage; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; +import static io.agora.rtc.video.VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15; +import static io.agora.rtc.video.VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; +import static io.agora.rtc.video.VideoEncoderConfiguration.STANDARD_BITRATE; +import static io.agora.rtc.video.VideoEncoderConfiguration.VD_640x360; + +/** + * This example demonstrates how to push a stream to an external address. + *

+ * Important: + * Users who push and pull streams cannot be in one channel, + * otherwise unexpected errors will occur. + */ +@Example( + index = 3, + group = ADVANCED, + name = R.string.item_rtmpstreaming, + actionId = R.id.action_mainFragment_to_RTMPStreaming, + tipsId = R.string.rtmpstreaming +) +public class RTMPStreaming extends BaseFragment implements View.OnClickListener { + private static final String TAG = RTMPStreaming.class.getSimpleName(); + + private LinearLayout llTransCode; + private Switch transCodeSwitch; + private FrameLayout fl_local, fl_remote; + private EditText et_url, et_channel; + private Button join, publish; + private RtcEngine engine; + private int myUid; + private boolean joined = false, publishing = false; + private VideoEncoderConfiguration.VideoDimensions dimensions = VD_640x360; + private LiveTranscoding transcoding; + /** + * Maximum number of users participating in transcoding (even number) + */ + private final int MAXUserCount = 2; + private LiveTranscoding.TranscodingUser localTranscodingUser; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_rtmp_streaming, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + llTransCode = view.findViewById(R.id.ll_TransCode); + transCodeSwitch = view.findViewById(R.id.transCode_Switch); + fl_local = view.findViewById(R.id.fl_local); + fl_remote = view.findViewById(R.id.fl_remote); + et_channel = view.findViewById(R.id.et_channel); + et_url = view.findViewById(R.id.et_url); + join = view.findViewById(R.id.btn_join); + join.setOnClickListener(this); + publish = view.findViewById(R.id.btn_publish); + publish.setOnClickListener(this); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) { + return; + } + try { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); + } + catch (Exception e) { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if (engine != null) { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + } + + @Override + public void onClick(View v) { + + if (v.getId() == R.id.btn_join) { + if (!joined) { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE, + Permission.Group.CAMERA + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } else { + engine.leaveChannel(); + transCodeSwitch.setEnabled(true); + joined = false; + join.setText(getString(R.string.join)); + publishing = false; + publish.setEnabled(false); + publish.setText(getString(R.string.publish)); + } + } else if (v.getId() == R.id.btn_publish) { + /**Ensure that the user joins a channel before calling this method.*/ + if (joined && !publishing) { + startPublish(); + } else if (joined && publishing) { + stopPublish(); + } + } + } + + private void joinChannel(String channelId) { + // Check if the context is valid + Context context = getContext(); + if (context == null) { + return; + } + // Create render view by RtcEngine + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + // Add to the local container + fl_local.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup local video to render your local camera preview + engine.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0)); + // Set audio route to speaker + engine.setDefaultAudioRoutetoSpeakerphone(true); + + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + // Enable video module + engine.enableVideo(); + // Setup video encoding configs + engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + ((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(), + VideoEncoderConfiguration.FRAME_RATE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate()), + STANDARD_BITRATE, + VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) + )); + /**Set up to play remote sound with receiver*/ + engine.setDefaultAudioRoutetoSpeakerphone(false); + engine.setEnableSpeakerphone(false); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + private void startPublish() { + if (transCodeSwitch.isChecked()) { + /**LiveTranscoding: A class for managing user-specific CDN live audio/video transcoding settings. + * See */ + transcoding = new LiveTranscoding(); + transcoding.width = dimensions.height; + transcoding.height = dimensions.width; + /**The transcodingUser class which defines the video properties of the user displaying the + * video in the CDN live. Agora supports a maximum of 17 transcoding users in a CDN live streaming channel. + * See */ + localTranscodingUser = new LiveTranscoding.TranscodingUser(); + localTranscodingUser.x = 0; + localTranscodingUser.y = 0; + localTranscodingUser.width = transcoding.width; + localTranscodingUser.height = transcoding.height / MAXUserCount; + localTranscodingUser.uid = myUid; + /**Adds a user displaying the video in CDN live. + * @return + * 0: Success. + * <0: Failure.*/ + int ret = transcoding.addUser(localTranscodingUser); + /**Sets the video layout and audio settings for CDN live. + * The SDK triggers the onTranscodingUpdated callback when you call this method to update + * the LiveTranscodingclass. If you call this method to set the LiveTranscoding class for + * the first time, the SDK does not trigger the onTranscodingUpdated callback. + * @param transcoding Sets the CDN live audio/video transcoding settings See + * + * @return + * 0: Success. + * <0: Failure. + * PS: + * This method applies to Live Broadcast only. + * Ensure that you enable the RTMP Converter service before using this function. See + * Prerequisites in Push Streams to CDN. + * Ensure that you call the setClientRole method and set the user role as the host. + * Ensure that you call the setLiveTranscoding method before calling the addPublishStreamUrl method.*/ + engine.setLiveTranscoding(transcoding); + } + /**Publishes the local stream to the CDN. + * The addPublishStreamUrl method call triggers the onRtmpStreamingStateChanged callback on + * the local client to report the state of adding a local stream to the CDN. + * @param url The CDN streaming URL in the RTMP format. The maximum length of this parameter + * is 1024 bytes. The URL address must not contain special characters, such as + * Chinese language characters. + * @param transcodingEnabled Sets whether transcoding is enabled/disabled. If you set this + * parameter as true, ensure that you call the setLiveTranscoding + * method before this method. + * true: Enable transcoding. To transcode the audio or video + * streams when publishing them to CDN live, often used for + * combining the audio and video streams of multiple hosts in CDN live. + * false: Disable transcoding. + * @return + * 0: Success. + * < 0: Failure. + * ERR_INVALID_ARGUMENT(2): Invalid parameter, usually because the URL address is null or the string length is 0. + * ERR_NOT_INITIALIZED(7): You have not initialized RtcEngine when publishing the stream. + * PS: + * Ensure that you enable the RTMP Converter service before using this function. See + * Prerequisites in Push Streams to CDN. + * This method applies to Live Broadcast only. + * Ensure that the user joins a channel before calling this method. + * This method adds only one stream HTTP/HTTPS URL address each time it is called.*/ + int code = engine.addPublishStreamUrl(et_url.getText().toString(), transCodeSwitch.isChecked()); + /**Prevent repeated entry*/ + publish.setEnabled(false); + /**Prevent duplicate clicks*/ + transCodeSwitch.setEnabled(false); + } + + private void stopPublish() { + /**Removes an RTMP stream from the CDN. + * This method removes the RTMP URL address (added by addPublishStreamUrl) from a CDN live + * stream. The SDK reports the result of this method call in the onRtmpStreamingStateChanged callback. + * @param url The RTMP URL address to be removed. The maximum length of this parameter is + * 1024 bytes. The URL address must not contain special characters, such as + * Chinese language characters. + * @return + * 0: Success. + * <0: Failure. + * PS: + * Ensure that you enable the RTMP Converter service before using this function. See + * Prerequisites in Push Streams to CDN. + * Ensure that the user joins a channel before calling this method. + * This method applies to Live Broadcast only. + * This method removes only one stream RTMP URL address each time it is called.*/ + int ret = engine.removePublishStreamUrl(et_url.getText().toString()); + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() { + @Override + public void run() { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + publish.setEnabled(true); + publish.setText(getString(R.string.publish)); + } + }); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Since v2.9.0. + * Occurs when the remote video state changes. + * PS: This callback does not work properly when the number of users (in the Communication + * profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the remote user whose video state changes. + * @param state State of the remote video: + * REMOTE_VIDEO_STATE_STOPPED(0): The remote video is in the default state, probably due + * to REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3), REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5), + * or REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7). + * REMOTE_VIDEO_STATE_STARTING(1): The first remote video packet is received. + * REMOTE_VIDEO_STATE_DECODING(2): The remote video stream is decoded and plays normally, + * probably due to REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY (2), + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4), REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6), + * or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9). + * REMOTE_VIDEO_STATE_FROZEN(3): The remote video is frozen, probably due to + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1) or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8). + * REMOTE_VIDEO_STATE_FAILED(4): The remote video fails to start, probably due to + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0). + * @param reason The reason of the remote video state change: + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0): Internal reasons. + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3): The local user stops receiving the remote + * video stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote + * video stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5): The remote user stops sending the video + * stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the video + * stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8): The remote media stream falls back to the + * audio-only stream due to poor network conditions. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9): The remote media stream switches + * back to the video stream after the network conditions improve. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method until + * the SDK triggers this callback.*/ + @Override + public void onRemoteVideoStateChanged(int uid, int state, int reason, int elapsed) { + super.onRemoteVideoStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteVideoStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Since v2.4.1 + * Occurs when the state of the RTMP streaming changes. + * This callback indicates the state of the RTMP streaming. When exceptions occur, you can + * troubleshoot issues by referring to the detailed error descriptions in the errCode parameter. + * @param url The RTMP URL address. + * @param state The RTMP streaming state: + * RTMP_STREAM_PUBLISH_STATE_IDLE(0): The RTMP streaming has not started or has ended. + * This state is also triggered after you remove an RTMP address from the CDN + * by calling removePublishStreamUrl. + * RTMP_STREAM_PUBLISH_STATE_CONNECTING(1): The SDK is connecting to Agora streaming server + * and the RTMP server. This state is triggered after you call the addPublishStreamUrl method. + * RTMP_STREAM_PUBLISH_STATE_RUNNING(2): The RTMP streaming publishes. The SDK successfully + * publishes the RTMP streaming and returns this state. + * RTMP_STREAM_PUBLISH_STATE_RECOVERING(3): The RTMP streaming is recovering. When exceptions + * occur to the CDN, or the streaming is interrupted, the SDK attempts to resume + * RTMP streaming and returns this state. + * 1:If the SDK successfully resumes the streaming, RTMP_STREAM_PUBLISH_STATE_RUNNING(2) + * returns. + * 2:If the streaming does not resume within 60 seconds or server errors occur, + * RTMP_STREAM_PUBLISH_STATE_FAILURE(4) returns. You can also reconnect to the + * server by calling the removePublishStreamUrl and addPublishStreamUrl methods. + * RTMP_STREAM_PUBLISH_STATE_FAILURE(4): The RTMP streaming fails. See the errCode parameter + * for the detailed error information. You can also call the addPublishStreamUrl + * method to publish the RTMP streaming again. + * @param errCode The detailed error information for streaming: + * RTMP_STREAM_PUBLISH_ERROR_OK(0): The RTMP streaming publishes successfully. + * RTMP_STREAM_PUBLISH_ERROR_INVALID_ARGUMEN(1): Invalid argument used. If, for example, + * you do not call the setLiveTranscoding method to configure the LiveTranscoding + * parameters before calling the addPublishStreamUrl method, the SDK returns + * this error. Check whether you set the parameters in the setLiveTranscoding method properly. + * RTMP_STREAM_PUBLISH_ERROR_ENCRYPTED_STREAM_NOT_ALLOWED(2): The RTMP streaming is + * encrypted and cannot be published. + * RTMP_STREAM_PUBLISH_ERROR_CONNECTION_TIMEOUT(3): Timeout for the RTMP streaming. Call + * the addPublishStreamUrl method to publish the streaming again. + * RTMP_STREAM_PUBLISH_ERROR_INTERNAL_SERVER_ERROR(4): An error occurs in Agora streaming + * server. Call the addPublishStreamUrl method to publish the streaming again. + * RTMP_STREAM_PUBLISH_ERROR_RTMP_SERVER_ERROR(5): An error occurs in the RTMP server. + * RTMP_STREAM_PUBLISH_ERROR_TOO_OFTEN(6): The RTMP streaming publishes too frequently. + * RTMP_STREAM_PUBLISH_ERROR_REACH_LIMIT(7): The host publishes more than 10 URLs. Delete + * the unnecessary URLs before adding new ones. + * RTMP_STREAM_PUBLISH_ERROR_NOT_AUTHORIZED(8): The host manipulates other hosts' URLs. + * Check your app logic. + * RTMP_STREAM_PUBLISH_ERROR_STREAM_NOT_FOUND(9): Agora server fails to find the RTMP + * streaming. + * RTMP_STREAM_PUBLISH_ERROR_FORMAT_NOT_SUPPORTED(10): The format of the RTMP streaming + * URL is not supported. Check whether the URL format is correct.*/ + @Override + public void onRtmpStreamingStateChanged(String url, int state, int errCode) { + super.onRtmpStreamingStateChanged(url, state, errCode); + Log.i(TAG, "onRtmpStreamingStateChanged->" + url + ", state->" + state + ", errCode->" + errCode); + if (state == Constants.RTMP_STREAM_PUBLISH_STATE_RUNNING) { + /**After confirming the successful push, make changes to the UI.*/ + publishing = true; + handler.post(() -> { + publish.setEnabled(true); + publish.setText(getString(R.string.stoppublish)); + }); + } else if (state == Constants.RTMP_STREAM_PUBLISH_STATE_FAILURE) { + /**if failed, make changes to the UI.*/ + publishing = true; + handler.post(() -> { + publish.setEnabled(true); + publish.setText(getString(R.string.publish)); + transCodeSwitch.setEnabled(true); + publishing = false; + }); + } else if (state == Constants.RTMP_STREAM_PUBLISH_STATE_IDLE) { + /**Push stream not started or ended, make changes to the UI.*/ + publishing = true; + handler.post(() -> { + publish.setEnabled(true); + publish.setText(getString(R.string.publish)); + transCodeSwitch.setEnabled(true); + publishing = false; + }); + } + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + handler.post(() -> + { + /**Display remote video stream*/ + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + surfaceView.setZOrderMediaOverlay(true); + if (fl_remote.getChildCount() > 0) { + fl_remote.removeAllViews(); + } + // Add to the remote container + fl_remote.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup remote video to render + engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, uid)); + }); + /**Determine whether to open transcoding service and whether the current number of + * transcoding users exceeds the maximum number of users*/ + if (transCodeSwitch.isChecked() && transcoding.getUserCount() < MAXUserCount) { + /**The transcoding images are arranged vertically according to the adding order*/ + LiveTranscoding.TranscodingUser transcodingUser = new LiveTranscoding.TranscodingUser(); + transcodingUser.x = 0; + transcodingUser.y = localTranscodingUser.height; + transcodingUser.width = transcoding.width; + transcodingUser.height = transcoding.height / MAXUserCount; + transcodingUser.uid = uid; + int ret = transcoding.addUser(transcodingUser); + /**refresh transCoding configuration*/ + engine.setLiveTranscoding(transcoding); + } + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + handler.post(new Runnable() { + @Override + public void run() { + /**Clear render view + Note: The video will stay at its last frame, to completely remove it you will need to + remove the SurfaceView from its parent*/ + engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); + if(transcoding != null) { + /**Removes a user from CDN live. + * @return + * 0: Success. + * < 0: Failure.*/ + int code = transcoding.removeUser(uid); + if (code == Constants.ERR_OK) { + /**refresh transCoding configuration*/ + engine.setLiveTranscoding(transcoding); + } + } + } + }); + } + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SendDataStream.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SendDataStream.java new file mode 100644 index 000000000..4a9a00279 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SendDataStream.java @@ -0,0 +1,503 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import java.nio.charset.Charset; + +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.models.DataStreamConfig; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; +import static io.agora.rtc.video.VideoEncoderConfiguration.STANDARD_BITRATE; + +@Example( + index = 23, + group = ADVANCED, + name = R.string.item_senddatastream, + actionId = R.id.action_mainFragment_senddatastream, + tipsId = R.string.senddatastream +) +public class SendDataStream extends BaseFragment implements View.OnClickListener +{ + public static final String TAG = SendDataStream.class.getSimpleName(); + private FrameLayout fl_local, fl_remote; + private Button send, join; + private EditText et_channel; + private RtcEngine engine; + private int myUid; + private boolean joined = false; + /** + * Meta data to be sent + */ + private byte[] data; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_send_datastream, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); + send = view.findViewById(R.id.btn_send); + send.setOnClickListener(this); + send.setEnabled(false); + join = view.findViewById(R.id.btn_join); + et_channel = view.findViewById(R.id.et_channel); + view.findViewById(R.id.btn_join).setOnClickListener(this); + fl_local = view.findViewById(R.id.fl_local); + fl_remote = view.findViewById(R.id.fl_remote); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) + { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + try + { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); + } + catch (Exception e) + { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() + { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if (engine != null) + { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + } + + @Override + public void onClick(View v) + { + if (v.getId() == R.id.btn_join) + { + if (!joined) + { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) + { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE, + Permission.Group.CAMERA + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } + else + { + joined = false; + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + send.setEnabled(false); + join.setText(getString(R.string.join)); + } + } + else if (v.getId() == R.id.btn_send) + { + /**Click once, the metadata is sent once. + * {@link SendDataStream#iMetadataObserver}. + * The metadata here can be flexibly replaced according to your own business.*/ + data = String.valueOf(System.currentTimeMillis()).getBytes(Charset.forName("UTF-8")); + DataStreamConfig dataStreamConfig = new DataStreamConfig(); + dataStreamConfig.ordered = true; + dataStreamConfig.syncWithAudio = true; + int streamId = engine.createDataStream(dataStreamConfig); + engine.sendStreamMessage(streamId, data); + } + } + + private void joinChannel(String channelId) + { + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + + // Create render view by RtcEngine + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + // Add to the local container + fl_local.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup local video to render your local camera preview + engine.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0)); + + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + // Enable video module + engine.enableVideo(); + // Setup video encoding configs + engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + ((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(), + VideoEncoderConfiguration.FRAME_RATE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate()), + STANDARD_BITRATE, + VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) + )); + /**Set up to play remote sound with receiver*/ + engine.setDefaultAudioRoutetoSpeakerphone(false); + engine.setEnableSpeakerphone(false); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) + { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) + { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() + { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) + { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) + { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) + { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) + { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() + { + @Override + public void run() + { + send.setEnabled(true); + join.setEnabled(true); + join.setText(getString(R.string.leave)); + } + }); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Since v2.9.0. + * Occurs when the remote video state changes. + * PS: This callback does not work properly when the number of users (in the Communication + * profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the remote user whose video state changes. + * @param state State of the remote video: + * REMOTE_VIDEO_STATE_STOPPED(0): The remote video is in the default state, probably due + * to REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3), REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5), + * or REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7). + * REMOTE_VIDEO_STATE_STARTING(1): The first remote video packet is received. + * REMOTE_VIDEO_STATE_DECODING(2): The remote video stream is decoded and plays normally, + * probably due to REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY (2), + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4), REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6), + * or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9). + * REMOTE_VIDEO_STATE_FROZEN(3): The remote video is frozen, probably due to + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1) or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8). + * REMOTE_VIDEO_STATE_FAILED(4): The remote video fails to start, probably due to + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0). + * @param reason The reason of the remote video state change: + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0): Internal reasons. + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3): The local user stops receiving the remote + * video stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote + * video stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5): The remote user stops sending the video + * stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the video + * stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8): The remote media stream falls back to the + * audio-only stream due to poor network conditions. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9): The remote media stream switches + * back to the video stream after the network conditions improve. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method until + * the SDK triggers this callback.*/ + @Override + public void onRemoteVideoStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteVideoStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteVideoStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) + { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + handler.post(() -> + { + /**Display remote video stream*/ + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + surfaceView.setZOrderMediaOverlay(true); + if (fl_remote.getChildCount() > 0) + { + fl_remote.removeAllViews(); + } + // Add to the remote container + fl_remote.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + // Setup remote video to render + engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, uid)); + }); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) + { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + handler.post(new Runnable() + { + @Override + public void run() + { + /**Clear render view + Note: The video will stay at its last frame, to completely remove it you will need to + remove the SurfaceView from its parent*/ + engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); + } + }); + } + + /** + * Occurs when the local user receives a remote data stream. + * The SDK triggers this callback when the local user receives the stream message that the remote user sends by calling the sendStreamMessage method. + * @param uid User ID of the remote user sending the data stream. + * @param streamId Stream ID. + * @param data Data received by the local user. + */ + @Override + public void onStreamMessage(int uid, int streamId, byte[] data) { + String string = new String(data, Charset.forName("UTF-8")); + handler.post(new Runnable() { + @Override + public void run() { + Toast.makeText(getContext(), String.format(getString(R.string.received), string), 300).show(); + } + }); + Log.i(TAG, "onStreamMessage:" + data); + } + + + /** + * Occurs when the local user fails to receive a remote data stream. + * The SDK triggers this callback when the local user fails to receive the stream message that the remote user sends by calling the sendStreamMessage method. + * @param uid User ID of the remote user sending the data stream. + * @param streamId Stream ID. + * @param error https://docs.agora.io/en/Video/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + * @param missed The number of lost messages. + * @param cached The number of incoming cached messages when the data stream is interrupted. + */ + @Override + public void onStreamMessageError(int uid, int streamId, int error, int missed, int cached) { + Log.e(TAG, "onStreamMessageError:" + error); + } + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SetAudioProfile.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SetAudioProfile.java new file mode 100644 index 000000000..2e13745ba --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SetAudioProfile.java @@ -0,0 +1,405 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.Spinner; +import android.widget.Switch; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.examples.basic.JoinChannelAudio; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.models.ChannelMediaOptions; + +import static io.agora.api.example.common.model.Examples.ADVANCED; + +@Example( + index = 13, + group = ADVANCED, + name = R.string.item_setaudioprofile, + actionId = R.id.action_mainFragment_to_SetAudioProfile, + tipsId = R.string.setaudioprofile +) +public class SetAudioProfile extends BaseFragment implements View.OnClickListener, CompoundButton.OnCheckedChangeListener { + private static final String TAG = JoinChannelAudio.class.getSimpleName(); + private Spinner audioProfileInput; + private Spinner audioScenarioInput; + private EditText et_channel; + private Button mute, join, speaker; + private Switch denoise; + private RtcEngine engine; + private int myUid; + private boolean joined = false; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + handler = new Handler(); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_set_audio_profile, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + et_channel = view.findViewById(R.id.et_channel); + audioProfileInput = view.findViewById(R.id.audio_profile_spinner); + audioScenarioInput = view.findViewById(R.id.audio_scenario_spinner); + view.findViewById(R.id.btn_join).setOnClickListener(this); + mute = view.findViewById(R.id.btn_mute); + mute.setOnClickListener(this); + speaker = view.findViewById(R.id.btn_speaker); + speaker.setOnClickListener(this); + denoise = view.findViewById(R.id.aidenoise); + denoise.setOnCheckedChangeListener(this); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) + { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + try + { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + String appId = getString(R.string.agora_app_id); + engine = RtcEngine.create(getContext().getApplicationContext(), appId, iRtcEngineEventHandler); + } + catch (Exception e) + { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() + { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if(engine != null) + { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + } + + + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean b) { + if (compoundButton.getId() == R.id.aidenoise){ + /** Enable deep learning noise suppression for local user. + * @since v3.3.0. + * + * @param enabled Whether or not to deep learning noise suppression for local user: + * - `true`: Enables deep learning noise suppression. + * - `false`: Disables deep learning noise suppression. + * @return + * - 0: Success. + * - -1: Failure. + */ + engine.enableDeepLearningDenoise(b); + } + } + + @Override + public void onClick(View v) + { + if (v.getId() == R.id.btn_join) + { + if (!joined) + { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) + { + joinChannel(channelId); + audioProfileInput.setEnabled(false); + audioScenarioInput.setEnabled(false); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + audioProfileInput.setEnabled(false); + audioScenarioInput.setEnabled(false); + }).start(); + } + else + { + joined = false; + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + join.setText(getString(R.string.join)); + speaker.setText(getString(R.string.speaker)); + speaker.setEnabled(false); + mute.setText(getString(R.string.closemicrophone)); + mute.setEnabled(false); + denoise.setEnabled(false); + audioProfileInput.setEnabled(true); + audioScenarioInput.setEnabled(true); + } + } + else if (v.getId() == R.id.btn_mute) + { + mute.setActivated(!mute.isActivated()); + mute.setText(getString(mute.isActivated() ? R.string.openmicrophone : R.string.closemicrophone)); + /**Turn off / on the microphone, stop / start local audio collection and push streaming.*/ + engine.muteLocalAudioStream(mute.isActivated()); + } + else if (v.getId() == R.id.btn_speaker) + { + speaker.setActivated(!speaker.isActivated()); + speaker.setText(getString(speaker.isActivated() ? R.string.earpiece : R.string.speaker)); + /**Turn off / on the speaker and change the audio playback route.*/ + engine.setEnableSpeakerphone(speaker.isActivated()); + } + } + + /** + * @param channelId Specify the channel name that you want to join. + * Users that input the same channel name join the same channel.*/ + private void joinChannel(String channelId) + { + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) + { + accessToken = null; + } + int profile = Constants.AudioProfile.valueOf(audioProfileInput.getSelectedItem().toString()).ordinal(); + int scenario = Constants.AudioScenario.valueOf(audioScenarioInput.getSelectedItem().toString()).ordinal(); + engine.setAudioProfile(profile, scenario); + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) + { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + /**IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() + { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) + { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) + { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) + { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) + { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() + { + @Override + public void run() + { + speaker.setEnabled(true); + mute.setEnabled(true); + join.setEnabled(true); + join.setText(getString(R.string.leave)); + denoise.setEnabled(true); + } + }); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) + { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) + { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + } + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SetVideoProfile.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SetVideoProfile.java new file mode 100644 index 000000000..2799d4282 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SetVideoProfile.java @@ -0,0 +1,529 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.Spinner; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import java.lang.reflect.Field; + +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; +import static io.agora.rtc.video.VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15; +import static io.agora.rtc.video.VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; +import static io.agora.rtc.video.VideoEncoderConfiguration.STANDARD_BITRATE; +import static io.agora.rtc.video.VideoEncoderConfiguration.VD_640x360; + +/**This demo demonstrates how to make a one-to-one video call*/ +@Example( + index = 21, + group = ADVANCED, + name = R.string.item_setvideoprofile, + actionId = R.id.action_mainFragment_to_set_video_profile, + tipsId = R.string.setvideoprofile +) +public class SetVideoProfile extends BaseFragment implements View.OnClickListener +{ + private static final String TAG = SetVideoProfile.class.getSimpleName(); + + private FrameLayout fl_local, fl_remote; + private Button join; + private EditText et_channel, et_bitrate; + private RtcEngine engine; + private Spinner dimension, framerate, orientation; + private int myUid; + private boolean joined = false; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_set_video_profile, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + et_channel = view.findViewById(R.id.et_channel); + view.findViewById(R.id.btn_join).setOnClickListener(this); + fl_local = view.findViewById(R.id.fl_local); + fl_remote = view.findViewById(R.id.fl_remote); + et_bitrate = view.findViewById(R.id.et_bitrate); + dimension = view.findViewById(R.id.dimension_spinner); + framerate = view.findViewById(R.id.frame_rate_spinner); + orientation = view.findViewById(R.id.orientation_spinner); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) + { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + try + { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); + } + catch (Exception e) + { + e.printStackTrace(); + getActivity().onBackPressed(); + } + String[] mItems = getResources().getStringArray(R.array.orientations); + String[] labels = new String[mItems.length]; + for(int i = 0;i arrayAdapter =new ArrayAdapter(context,android.R.layout.simple_spinner_dropdown_item, labels); + orientation.setAdapter(arrayAdapter); + fetchGlobalSettings(); + } + + private void fetchGlobalSettings(){ + String[] mItems = getResources().getStringArray(R.array.orientations); + String selectedItem = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation(); + int i = 0; + if(selectedItem!=null){ + for(String item : mItems){ + if(selectedItem.equals(item)){ + break; + } + i++; + } + } + orientation.setSelection(i); + mItems = getResources().getStringArray(R.array.fps); + selectedItem = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate(); + i = 0; + if(selectedItem!=null){ + for(String item : mItems){ + if(selectedItem.equals(item)){ + break; + } + i++; + } + } + framerate.setSelection(i); + mItems = getResources().getStringArray(R.array.dimensions); + selectedItem = ((MainApplication) getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimension(); + i = 0; + if(selectedItem!=null){ + for(String item : mItems){ + if(selectedItem.equals(item)){ + break; + } + i++; + } + } + dimension.setSelection(i); + } + + @Override + public void onDestroy() + { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if(engine != null) + { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + } + + @Override + public void onClick(View v) + { + if (v.getId() == R.id.btn_join) + { + if (!joined) + { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) + { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE, + Permission.Group.CAMERA + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } + else + { + joined = false; + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + join.setText(getString(R.string.join)); + et_bitrate.setEnabled(true); + dimension.setEnabled(true); + framerate.setEnabled(true); + orientation.setEnabled(true); + } + } + } + + private void joinChannel(String channelId) + { + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + + // Create render view by RtcEngine + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + if(fl_local.getChildCount() > 0) + { + fl_local.removeAllViews(); + } + // Add to the local container + fl_local.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup local video to render your local camera preview + engine.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0)); + // Set audio route to microPhone + engine.setDefaultAudioRoutetoSpeakerphone(false); + + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + // Enable video module + engine.enableVideo(); + // Setup video encoding configs + + VideoEncoderConfiguration.VideoDimensions value = VD_640x360; + try { + Field tmp = VideoEncoderConfiguration.class.getDeclaredField(dimension.getSelectedItem().toString()); + tmp.setAccessible(true); + value = (VideoEncoderConfiguration.VideoDimensions) tmp.get(null); + } catch (NoSuchFieldException e) { + Log.e("Field", "Can not find field " + dimension.getSelectedItem().toString()); + } catch (IllegalAccessException e) { + Log.e("Field", "Could not access field " + dimension.getSelectedItem().toString()); + } + + engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + value, + VideoEncoderConfiguration.FRAME_RATE.valueOf(framerate.getSelectedItem().toString()), + Integer.valueOf(et_bitrate.getText().toString()), + VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(orientation.getSelectedItem().toString()) + )); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) + { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) + { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() + { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) + { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) + { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) + { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) + { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() + { + @Override + public void run() + { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + et_bitrate.setEnabled(false); + framerate.setEnabled(false); + orientation.setEnabled(false); + dimension.setEnabled(false); + } + }); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Since v2.9.0. + * Occurs when the remote video state changes. + * PS: This callback does not work properly when the number of users (in the Communication + * profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the remote user whose video state changes. + * @param state State of the remote video: + * REMOTE_VIDEO_STATE_STOPPED(0): The remote video is in the default state, probably due + * to REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3), REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5), + * or REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7). + * REMOTE_VIDEO_STATE_STARTING(1): The first remote video packet is received. + * REMOTE_VIDEO_STATE_DECODING(2): The remote video stream is decoded and plays normally, + * probably due to REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY (2), + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4), REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6), + * or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9). + * REMOTE_VIDEO_STATE_FROZEN(3): The remote video is frozen, probably due to + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1) or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8). + * REMOTE_VIDEO_STATE_FAILED(4): The remote video fails to start, probably due to + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0). + * @param reason The reason of the remote video state change: + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0): Internal reasons. + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3): The local user stops receiving the remote + * video stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote + * video stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5): The remote user stops sending the video + * stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the video + * stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8): The remote media stream falls back to the + * audio-only stream due to poor network conditions. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9): The remote media stream switches + * back to the video stream after the network conditions improve. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method until + * the SDK triggers this callback.*/ + @Override + public void onRemoteVideoStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteVideoStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteVideoStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) + { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + handler.post(() -> + { + /**Display remote video stream*/ + SurfaceView surfaceView = null; + if (fl_remote.getChildCount() > 0) + { + fl_remote.removeAllViews(); + } + // Create render view by RtcEngine + surfaceView = RtcEngine.CreateRendererView(context); + surfaceView.setZOrderMediaOverlay(true); + // Add to the remote container + fl_remote.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + // Setup remote video to render + engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, uid)); + }); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) + { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + handler.post(new Runnable() { + @Override + public void run() { + /**Clear render view + Note: The video will stay at its last frame, to completely remove it you will need to + remove the SurfaceView from its parent*/ + engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); + } + }); + } + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/StreamEncrypt.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/StreamEncrypt.java new file mode 100644 index 000000000..a1362b1d0 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/StreamEncrypt.java @@ -0,0 +1,369 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.api.streamencrypt.PacketProcessor; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; +import static io.agora.rtc.video.VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15; +import static io.agora.rtc.video.VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; +import static io.agora.rtc.video.VideoEncoderConfiguration.STANDARD_BITRATE; +import static io.agora.rtc.video.VideoEncoderConfiguration.VD_640x360; + +/**This example demonstrates how to use a custom encryption scheme to encrypt audio and video streams.*/ +@Example( + index = 12, + group = ADVANCED, + name = R.string.item_streamencrypt, + actionId = R.id.action_mainFragment_to_StreamEncrypt, + tipsId = R.string.streamencrypt +) +public class StreamEncrypt extends BaseFragment implements View.OnClickListener +{ + private static final String TAG = StreamEncrypt.class.getSimpleName(); + + private FrameLayout fl_local, fl_remote; + private Button join; + private EditText et_channel; + private RtcEngine engine; + private int myUid; + private boolean joined = false; + private PacketProcessor packetProcessor = new PacketProcessor(); + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_stream_encrypt, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + et_channel = view.findViewById(R.id.et_channel); + view.findViewById(R.id.btn_join).setOnClickListener(this); + fl_local = view.findViewById(R.id.fl_local); + fl_remote = view.findViewById(R.id.fl_remote); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) + { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + try + { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); + } + catch (Exception e) + { + e.printStackTrace(); + getActivity().onBackPressed(); + } + /**register AgoraPacketObserver for encrypt/decrypt stream*/ + packetProcessor.registerProcessing(); + } + + @Override + public void onDestroy() + { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if(engine != null) + { + engine.leaveChannel(); + } + /**unregister AgoraPacketObserver*/ + packetProcessor.unregisterProcessing(); + handler.post(RtcEngine::destroy); + engine = null; + } + + @Override + public void onClick(View v) + { + if (v.getId() == R.id.btn_join) + { + if (!joined) + { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) + { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE, + Permission.Group.CAMERA + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } + else + { + joined = false; + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + join.setText(getString(R.string.join)); + } + } + } + + private void joinChannel(String channelId) + { + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + + // Create render view by RtcEngine + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + // Add to the local container + fl_local.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup local video to render your local camera preview + engine.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0)); + /**Set up to play remote sound with receiver*/ + engine.setDefaultAudioRoutetoSpeakerphone(false); + engine.setEnableSpeakerphone(false); + + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + // Enable video module + engine.enableVideo(); + // Setup video encoding configs + engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + ((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(), + VideoEncoderConfiguration.FRAME_RATE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate()), + STANDARD_BITRATE, + VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) + )); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) + { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) + { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() + { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) + { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) + { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) + { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) + { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() + { + @Override + public void run() + { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + } + }); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) + { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + handler.post(() -> + { + /**Display remote video stream*/ + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + surfaceView.setZOrderMediaOverlay(true); + if (fl_remote.getChildCount() > 0) + { + fl_remote.removeAllViews(); + } + // Add to the remote container + fl_remote.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup remote video to render + engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, uid)); + }); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) + { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + handler.post(new Runnable() { + @Override + public void run() { + /**Clear render view + Note: The video will stay at its last frame, to completely remove it you will need to + remove the SurfaceView from its parent*/ + engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); + } + }); + } + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SuperResolution.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SuperResolution.java new file mode 100644 index 000000000..8dfee2963 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SuperResolution.java @@ -0,0 +1,488 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; +import static io.agora.rtc.video.VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15; +import static io.agora.rtc.video.VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; +import static io.agora.rtc.video.VideoEncoderConfiguration.STANDARD_BITRATE; +import static io.agora.rtc.video.VideoEncoderConfiguration.VD_640x360; + +/**This demo demonstrates how to make a one-to-one video call*/ +@Example( + index = 21, + group = ADVANCED, + name = R.string.item_superresolution, + actionId = R.id.action_mainFragment_to_superResolution, + tipsId = R.string.superresolution +) +public class SuperResolution extends BaseFragment implements View.OnClickListener +{ + private static final String TAG = SuperResolution.class.getSimpleName(); + + private FrameLayout fl_local, fl_remote; + private Button join, btnSuperResolution; + private EditText et_channel; + private RtcEngine engine; + private int myUid; + private int remoteUid; + private boolean joined = false; + private boolean enableSuperResolution = false; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_super_resolution, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + btnSuperResolution = view.findViewById(R.id.btn_super_resolution); + btnSuperResolution.setEnabled(false); + et_channel = view.findViewById(R.id.et_channel); + view.findViewById(R.id.btn_join).setOnClickListener(this); + view.findViewById(R.id.btn_super_resolution).setOnClickListener(this); + fl_local = view.findViewById(R.id.fl_local); + fl_remote = view.findViewById(R.id.fl_remote); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) + { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + try + { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); + } + catch (Exception e) + { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() + { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if(engine != null) + { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + } + + @Override + public void onClick(View v) + { + if (v.getId() == R.id.btn_join) + { + if (!joined) + { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) + { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE, + Permission.Group.CAMERA + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } + else + { + joined = false; + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + join.setText(getString(R.string.join)); + } + } + else if(v.getId() == R.id.btn_super_resolution){ + engine.enableRemoteSuperResolution(remoteUid, !enableSuperResolution); + } + } + + private void joinChannel(String channelId) + { + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + + // Create render view by RtcEngine + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + if(fl_local.getChildCount() > 0) + { + fl_local.removeAllViews(); + } + // Add to the local container + fl_local.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup local video to render your local camera preview + engine.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0)); + // Set audio route to microPhone + engine.setDefaultAudioRoutetoSpeakerphone(false); + + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + // Enable video module + engine.enableVideo(); + // Setup video encoding configs + engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + ((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(), + VideoEncoderConfiguration.FRAME_RATE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate()), + STANDARD_BITRATE, + VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) + )); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) + { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) + { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() + { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) + { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) + { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) + { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) + { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() + { + @Override + public void run() + { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + } + }); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Since v2.9.0. + * Occurs when the remote video state changes. + * PS: This callback does not work properly when the number of users (in the Communication + * profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the remote user whose video state changes. + * @param state State of the remote video: + * REMOTE_VIDEO_STATE_STOPPED(0): The remote video is in the default state, probably due + * to REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3), REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5), + * or REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7). + * REMOTE_VIDEO_STATE_STARTING(1): The first remote video packet is received. + * REMOTE_VIDEO_STATE_DECODING(2): The remote video stream is decoded and plays normally, + * probably due to REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY (2), + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4), REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6), + * or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9). + * REMOTE_VIDEO_STATE_FROZEN(3): The remote video is frozen, probably due to + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1) or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8). + * REMOTE_VIDEO_STATE_FAILED(4): The remote video fails to start, probably due to + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0). + * @param reason The reason of the remote video state change: + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0): Internal reasons. + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3): The local user stops receiving the remote + * video stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote + * video stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5): The remote user stops sending the video + * stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the video + * stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8): The remote media stream falls back to the + * audio-only stream due to poor network conditions. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9): The remote media stream switches + * back to the video stream after the network conditions improve. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method until + * the SDK triggers this callback.*/ + @Override + public void onRemoteVideoStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteVideoStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteVideoStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) + { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + handler.post(() -> + { + /**Display remote video stream*/ + SurfaceView surfaceView = null; + if (fl_remote.getChildCount() > 0) + { + fl_remote.removeAllViews(); + } + // Create render view by RtcEngine + surfaceView = RtcEngine.CreateRendererView(context); + surfaceView.setZOrderMediaOverlay(true); + // Add to the remote container + fl_remote.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + // Setup remote video to render + engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, uid)); + remoteUid = uid; + btnSuperResolution.setEnabled(true); + }); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) + { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + handler.post(new Runnable() { + @Override + public void run() { + /**Clear render view + Note: The video will stay at its last frame, to completely remove it you will need to + remove the SurfaceView from its parent*/ + engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); + btnSuperResolution.setEnabled(false); + } + }); + } + + /** + * + * @param uid remote user id + * @param enabled updated status of super resolution + * @param reason possible reasons are: + * SR_STATE_REASON_SUCCESS(0) + * SR_STATE_REASON_STREAM_OVER_LIMITATION(1) + * SR_STATE_REASON_USER_COUNT_OVER_LIMITATION(2) + * SR_STATE_REASON_DEVICE_NOT_SUPPORTED(3) + */ + @Override + public void onUserSuperResolutionEnabled(int uid, boolean enabled, int reason) { + if(uid == 0 && !enabled && reason == 3){ + showLongToast(String.format("Unfortunately, Super Resolution can't enabled because your device doesn't support this feature.")); + return; + } + if(remoteUid == uid){ + if(reason!=0){ + showLongToast(String.format("Super Resolution can't enabled because of reason code: %d", reason)); + } + enableSuperResolution = enabled; + btnSuperResolution.setText(enableSuperResolution?getText(R.string.closesuperr):getText(R.string.opensuperr)); + } + } + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SwitchCameraScreenShare.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SwitchCameraScreenShare.java new file mode 100644 index 000000000..29a0d6935 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SwitchCameraScreenShare.java @@ -0,0 +1,553 @@ +package io.agora.api.example.examples.advanced; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.media.projection.MediaProjectionManager; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.RelativeLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import io.agora.advancedvideo.externvideosource.ExternalVideoInputManager; +import io.agora.advancedvideo.externvideosource.ExternalVideoInputService; +import io.agora.advancedvideo.externvideosource.IExternalVideoInputService; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.mediaio.AgoraDefaultSource; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static android.app.Activity.RESULT_OK; +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.rtc.Constants.REMOTE_VIDEO_STATE_STARTING; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_FIT; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; +import static io.agora.api.component.Constant.ENGINE; +import static io.agora.api.component.Constant.TEXTUREVIEW; + +/** + * This example demonstrates how video can be flexibly switched between the camera stream and the + * screen share stream during an audio-video call. + */ +@Example( + index = 7, + group = ADVANCED, + name = R.string.item_cameraorscreen, + actionId = R.id.action_mainFragment_to_SwitchCameraScreenShare, + tipsId = R.string.switchcamerascreen +) +public class SwitchCameraScreenShare extends BaseFragment implements View.OnClickListener { + private static final String TAG = SwitchCameraScreenShare.class.getSimpleName(); + + private FrameLayout fl_remote; + private RelativeLayout fl_local; + private Button join, renderMode, camera, screenShare; + private EditText et_channel; + private int myUid, remoteUid = -1; + private boolean joined = false; + private static final int PROJECTION_REQ_CODE = 1 << 2; + private static final int DEFAULT_SHARE_FRAME_RATE = 15; + private IExternalVideoInputService mService; + private VideoInputServiceConnection mServiceConnection; + private int curRenderMode = RENDER_MODE_HIDDEN; + private VideoEncoderConfiguration.ORIENTATION_MODE curMirrorMode = + VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_switch_camera_screenshare, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + renderMode = view.findViewById(R.id.renderMode); + camera = view.findViewById(R.id.camera); + screenShare = view.findViewById(R.id.screenShare); + et_channel = view.findViewById(R.id.et_channel); + fl_remote = view.findViewById(R.id.fl_remote); + fl_local = view.findViewById(R.id.fl_local); + join.setOnClickListener(this); + renderMode.setOnClickListener(this); + camera.setOnClickListener(this); + screenShare.setOnClickListener(this); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) { + return; + } + try { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + ENGINE = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); + } + catch (Exception e) { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == PROJECTION_REQ_CODE && resultCode == RESULT_OK) { + camera.setEnabled(true); + screenShare.setEnabled(false); + try { + DisplayMetrics metrics = new DisplayMetrics(); + getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics); + + float percent = 0.f; + float hp = ((float) metrics.heightPixels) - 1920.f; + float wp = ((float) metrics.widthPixels) - 1080.f; + + if (hp < wp) { + percent = (((float) metrics.widthPixels) - 1080.f) / ((float) metrics.widthPixels); + } else { + percent = (((float) metrics.heightPixels) - 1920.f) / ((float) metrics.heightPixels); + } + metrics.heightPixels = (int) (((float) metrics.heightPixels) - (metrics.heightPixels * percent)); + metrics.widthPixels = (int) (((float) metrics.widthPixels) - (metrics.widthPixels * percent)); + + data.putExtra(ExternalVideoInputManager.FLAG_SCREEN_WIDTH, metrics.widthPixels); + data.putExtra(ExternalVideoInputManager.FLAG_SCREEN_HEIGHT, metrics.heightPixels); + data.putExtra(ExternalVideoInputManager.FLAG_SCREEN_DPI, (int) metrics.density); + data.putExtra(ExternalVideoInputManager.FLAG_FRAME_RATE, DEFAULT_SHARE_FRAME_RATE); + setVideoConfig(ExternalVideoInputManager.TYPE_SCREEN_SHARE, metrics.widthPixels, metrics.heightPixels); + mService.setExternalVideoInput(ExternalVideoInputManager.TYPE_SCREEN_SHARE, data); + } + catch (RemoteException e) { + e.printStackTrace(); + } + } + } + + @Override + public void onDestroy() { + unbindVideoService(); + TEXTUREVIEW = null; + /**leaveChannel and Destroy the RtcEngine instance*/ + if (ENGINE != null) { + ENGINE.leaveChannel(); + } + handler.post(RtcEngine::destroy); + ENGINE = null; + super.onDestroy(); + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.btn_join) { + if (!joined) { + CommonUtil.hideInputBoard(getActivity(), et_channel); + /**Instantiate the view ready to display the local preview screen*/ + TEXTUREVIEW = new TextureView(getContext()); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE, + Permission.Group.CAMERA + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } else { + joined = false; + join.setText(getString(R.string.join)); + camera.setEnabled(false); + screenShare.setEnabled(false); + fl_remote.removeAllViews(); + fl_local.removeAllViews(); + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + ENGINE.leaveChannel(); + TEXTUREVIEW = null; + unbindVideoService(); + } + } else if (v.getId() == R.id.renderMode) { + if (remoteUid == -1) { + return; + } + if (curRenderMode == RENDER_MODE_HIDDEN) { + curRenderMode = RENDER_MODE_FIT; + renderMode.setText(String.format(getString(R.string.rendermode), getString(R.string.fit))); + } else if (curRenderMode == RENDER_MODE_FIT) { + curRenderMode = RENDER_MODE_HIDDEN; + renderMode.setText(String.format(getString(R.string.rendermode), getString(R.string.hidden))); + } +// setRemotePreview(getContext()); +// ENGINE.setRemoteRenderMode(remoteUid, curRenderMode, curMirrorMode.getValue()); + } else if (v.getId() == R.id.camera) { + unbindVideoService(); + handler.postDelayed(() -> { + /**setVideoSource must be called in {@link ExternalVideoInputManager.ExternalVideoInputThread#release()} + * after calling. Here the handler delay is used to guarantee this process. + * Developers can flexibly call them according to their own business logic*/ + ENGINE.setVideoSource(new AgoraDefaultSource()); + addLocalPreview(); + camera.setEnabled(false); + screenShare.setEnabled(true); + }, 1000); + } else if (v.getId() == R.id.screenShare) { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) { + /**remove local preview*/ + fl_local.removeAllViews(); + /**start input service*/ + bindVideoService(); + } else { + showAlert(getString(R.string.lowversiontip)); + } + } + } + + private void setVideoConfig(int sourceType, int width, int height) { + switch (sourceType) { + case ExternalVideoInputManager.TYPE_LOCAL_VIDEO: + case ExternalVideoInputManager.TYPE_SCREEN_SHARE: + curMirrorMode = VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_FIXED_PORTRAIT; + break; + default: + curMirrorMode = VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; + break; + } + + Log.e(TAG, "SDK encoding ->width:" + width + ",height:" + height); + /**Setup video stream encoding configs*/ + ENGINE.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + new VideoEncoderConfiguration.VideoDimensions(width, height), + VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15, + VideoEncoderConfiguration.STANDARD_BITRATE, curMirrorMode + )); + +// ENGINE.setParameters("{\"rtc.log_filter\": 65535}"); + } + + private void addLocalPreview() { + // Check if the context is valid + Context context = getContext(); + if (context == null) { + return; + } + + // Create render view by RtcEngine + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + if (fl_local.getChildCount() > 0) { + fl_local.removeAllViews(); + } + // Add to the local container + fl_local.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup local video to render your local camera preview + ENGINE.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0)); + } + + private void setRemotePreview(Context context) { + /**Display remote video stream*/ + SurfaceView remoteSurfaceView = RtcEngine.CreateRendererView(context); + remoteSurfaceView.setZOrderMediaOverlay(true); + if (fl_remote.getChildCount() > 0) { + fl_remote.removeAllViews(); + } + fl_remote.addView(remoteSurfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + /**Setup remote video to render*/ + ENGINE.setupRemoteVideo(new VideoCanvas(remoteSurfaceView, curRenderMode, remoteUid)); + } + + private void joinChannel(String channelId) { + addLocalPreview(); + + ENGINE.setParameters("{\"che.video.mobile_1080p\":true}"); + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + ENGINE.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**Sets the role of a user (Live Broadcast only).*/ + ENGINE.setClientRole(Constants.CLIENT_ROLE_BROADCASTER); + /**Enable video module*/ + ENGINE.enableVideo(); + ENGINE.setVideoSource(new AgoraDefaultSource()); + /**Set up to play remote sound with receiver*/ + ENGINE.setDefaultAudioRoutetoSpeakerphone(false); + ENGINE.setEnableSpeakerphone(false); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = ENGINE.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + private void bindVideoService() { + Intent intent = new Intent(); + intent.setClass(getContext(), ExternalVideoInputService.class); + mServiceConnection = new VideoInputServiceConnection(); + getContext().bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); + } + + private void unbindVideoService() { + if (mServiceConnection != null) { + getContext().unbindService(mServiceConnection); + mServiceConnection = null; + } + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + ENGINE.uploadLogFile(); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(() -> { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + camera.setEnabled(false); + screenShare.setEnabled(true); + }); + } + + @Override + public void onLocalVideoStateChanged(int localVideoState, int error) { + super.onLocalVideoStateChanged(localVideoState, error); + if (localVideoState == 1) { + Log.e(TAG, "launch successfully"); + } + } + + /**Since v2.9.0. + * Occurs when the remote video state changes. + * PS: This callback does not work properly when the number of users (in the Communication + * profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the remote user whose video state changes. + * @param state State of the remote video: + * REMOTE_VIDEO_STATE_STOPPED(0): The remote video is in the default state, probably due + * to REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3), REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5), + * or REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7). + * REMOTE_VIDEO_STATE_STARTING(1): The first remote video packet is received. + * REMOTE_VIDEO_STATE_DECODING(2): The remote video stream is decoded and plays normally, + * probably due to REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY (2), + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4), REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6), + * or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9). + * REMOTE_VIDEO_STATE_FROZEN(3): The remote video is frozen, probably due to + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1) or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8). + * REMOTE_VIDEO_STATE_FAILED(4): The remote video fails to start, probably due to + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0). + * @param reason The reason of the remote video state change: + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0): Internal reasons. + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3): The local user stops receiving the remote + * video stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote + * video stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5): The remote user stops sending the video + * stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the video + * stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8): The remote media stream falls back to the + * audio-only stream due to poor network conditions. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9): The remote media stream switches + * back to the video stream after the network conditions improve. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method until + * the SDK triggers this callback.*/ + @Override + public void onRemoteVideoStateChanged(int uid, int state, int reason, int elapsed) { + super.onRemoteVideoStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteVideoStateChanged:uid->" + uid + ", state->" + state); + if (state == REMOTE_VIDEO_STATE_STARTING) { + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + handler.post(() -> + { + remoteUid = uid; + renderMode.setEnabled(true); + renderMode.setText(String.format(getString(R.string.rendermode), getString(R.string.hidden))); + curRenderMode = RENDER_MODE_HIDDEN; + setRemotePreview(context); + }); + } + } + + @Override + public void onRemoteVideoStats(RemoteVideoStats stats) { + super.onRemoteVideoStats(stats); + Log.d(TAG, "onRemoteVideoStats: width:" + stats.width + " x height:" + stats.height); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + handler.post(() -> { + /**Clear render view + Note: The video will stay at its last frame, to completely remove it you will need to + remove the SurfaceView from its parent*/ + ENGINE.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); + fl_remote.removeAllViews(); + }); + } + }; + + private class VideoInputServiceConnection implements ServiceConnection { + @Override + public void onServiceConnected(ComponentName componentName, IBinder iBinder) { + mService = (IExternalVideoInputService) iBinder; + /**Start the screen recording service of the system*/ + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { + MediaProjectionManager mpm = (MediaProjectionManager) + getContext().getSystemService(Context.MEDIA_PROJECTION_SERVICE); + Intent intent = mpm.createScreenCaptureIntent(); + startActivityForResult(intent, PROJECTION_REQ_CODE); + } + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + mService = null; + } + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SwitchExternalVideo.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SwitchExternalVideo.java new file mode 100644 index 000000000..a95451fcc --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/SwitchExternalVideo.java @@ -0,0 +1,509 @@ +package io.agora.api.example.examples.advanced; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.media.projection.MediaProjectionManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.IBinder; +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Surface; +import android.view.SurfaceView; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.RelativeLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import java.io.File; + +import io.agora.advancedvideo.externvideosource.ExternalVideoInputManager; +import io.agora.advancedvideo.externvideosource.ExternalVideoInputService; +import io.agora.advancedvideo.externvideosource.IExternalVideoInputService; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static android.app.Activity.RESULT_OK; +import static io.agora.api.component.Constant.ENGINE; +import static io.agora.api.component.Constant.TEXTUREVIEW; +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.rtc.Constants.REMOTE_VIDEO_STATE_STARTING; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; + +/** + * This example demonstrates how to switch the external video source. The implementation method is + * similar to PushExternalVideo, all by rendering the external video to a TextureId + * (the specific form is Surface{@link io.agora.advancedvideo.externvideosource.IExternalVideoInput#onVideoInitialized(Surface)}), + * and then calling consumeTextureFrame in a loop to push the stream. + */ +@Example( + index = 6, + group = ADVANCED, + name = R.string.item_switchexternal, + actionId = R.id.action_mainFragment_to_SwitchExternalVideo, + tipsId = R.string.switchexternalvideo +) +public class SwitchExternalVideo extends BaseFragment implements View.OnClickListener { + private static final String TAG = SwitchExternalVideo.class.getSimpleName(); + + private FrameLayout fl_remote; + private RelativeLayout fl_local; + private Button join, localVideo, screenShare; + private EditText et_channel; + private int myUid; + private boolean joined = false; + private static final String VIDEO_NAME = "localvideo.mp4"; + private static final int PROJECTION_REQ_CODE = 1 << 2; + private static final int DEFAULT_SHARE_FRAME_RATE = 15; + /** + * The developers should defines their video dimension, for the + * video info cannot be obtained before the video is extracted. + */ + private static final int LOCAL_VIDEO_WIDTH = 1280; + private static final int LOCAL_VIDEO_HEIGHT = 720; + private String mLocalVideoPath; + private boolean mLocalVideoExists = false; + private IExternalVideoInputService mService; + private VideoInputServiceConnection mServiceConnection; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_switch_external_video, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + localVideo = view.findViewById(R.id.localVideo); + screenShare = view.findViewById(R.id.screenShare); + et_channel = view.findViewById(R.id.et_channel); + fl_remote = view.findViewById(R.id.fl_remote); + fl_local = view.findViewById(R.id.fl_local); + join.setOnClickListener(this); + localVideo.setOnClickListener(this); + screenShare.setOnClickListener(this); + checkLocalVideo(); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) { + return; + } + try { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + ENGINE = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); + } + catch (Exception e) { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == PROJECTION_REQ_CODE && resultCode == RESULT_OK) { + try { + DisplayMetrics metrics = new DisplayMetrics(); + getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics); + data.putExtra(ExternalVideoInputManager.FLAG_SCREEN_WIDTH, metrics.widthPixels); + data.putExtra(ExternalVideoInputManager.FLAG_SCREEN_HEIGHT, metrics.heightPixels); + data.putExtra(ExternalVideoInputManager.FLAG_SCREEN_DPI, (int) metrics.density); + data.putExtra(ExternalVideoInputManager.FLAG_FRAME_RATE, DEFAULT_SHARE_FRAME_RATE); + + setVideoConfig(ExternalVideoInputManager.TYPE_SCREEN_SHARE, metrics.widthPixels, metrics.heightPixels); + mService.setExternalVideoInput(ExternalVideoInputManager.TYPE_SCREEN_SHARE, data); + } + catch (RemoteException e) { + e.printStackTrace(); + } + } + } + + @Override + public void onDestroy() { + unbindVideoService(); + TEXTUREVIEW = null; + /**leaveChannel and Destroy the RtcEngine instance*/ + if (ENGINE != null) { + ENGINE.leaveChannel(); + } + handler.post(RtcEngine::destroy); + ENGINE = null; + super.onDestroy(); + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.btn_join) { + if (!joined) { + CommonUtil.hideInputBoard(getActivity(), et_channel); + /**Instantiate the view ready to display the local preview screen*/ + TEXTUREVIEW = new TextureView(getContext()); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE, + Permission.Group.CAMERA + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } else { + joined = false; + join.setText(getString(R.string.join)); + localVideo.setEnabled(false); + screenShare.setEnabled(false); + fl_remote.removeAllViews(); + fl_local.removeAllViews(); + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + ENGINE.leaveChannel(); + TEXTUREVIEW = null; + unbindVideoService(); + } + } else if (v.getId() == R.id.localVideo) { + try { + Intent intent = new Intent(); + setVideoConfig(ExternalVideoInputManager.TYPE_LOCAL_VIDEO, LOCAL_VIDEO_WIDTH, LOCAL_VIDEO_HEIGHT); + intent.putExtra(ExternalVideoInputManager.FLAG_VIDEO_PATH, mLocalVideoPath); + if (mService.setExternalVideoInput(ExternalVideoInputManager.TYPE_LOCAL_VIDEO, intent)) { + fl_local.removeAllViews(); + fl_local.addView(TEXTUREVIEW, + RelativeLayout.LayoutParams.MATCH_PARENT, + RelativeLayout.LayoutParams.MATCH_PARENT); + } + } + catch (RemoteException e) { + e.printStackTrace(); + } + } else if (v.getId() == R.id.screenShare) { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) { + /**remove local preview*/ + fl_local.removeAllViews(); + /***/ + MediaProjectionManager mpm = (MediaProjectionManager) + getContext().getSystemService(Context.MEDIA_PROJECTION_SERVICE); + Intent intent = mpm.createScreenCaptureIntent(); + startActivityForResult(intent, PROJECTION_REQ_CODE); + } else { + showAlert(getString(R.string.lowversiontip)); + } + } + } + + private boolean checkLocalVideo() { + File dir = getContext().getExternalFilesDir(Environment.DIRECTORY_MOVIES); + File videoFile = new File(dir, VIDEO_NAME); + mLocalVideoPath = videoFile.getAbsolutePath(); + mLocalVideoExists = videoFile.exists(); + if (!mLocalVideoExists) { + showAlert(String.format(getString(R.string.alert_no_local_video_message), mLocalVideoPath)); + } + return mLocalVideoExists; + } + + private void setVideoConfig(int sourceType, int width, int height) { + VideoEncoderConfiguration.ORIENTATION_MODE mode; + switch (sourceType) { + case ExternalVideoInputManager.TYPE_LOCAL_VIDEO: + case ExternalVideoInputManager.TYPE_SCREEN_SHARE: + mode = VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_FIXED_PORTRAIT; + break; + default: + mode = VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; + break; + } + /**Setup video stream encoding configs*/ + ENGINE.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + new VideoEncoderConfiguration.VideoDimensions(width, height), + VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15, + VideoEncoderConfiguration.STANDARD_BITRATE, mode + )); + } + + private void joinChannel(String channelId) { + // Check if the context is valid + Context context = getContext(); + if (context == null) { + return; + } + + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + ENGINE.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**Sets the role of a user (Live Broadcast only).*/ + ENGINE.setClientRole(Constants.CLIENT_ROLE_BROADCASTER); + /**Enable video module*/ + ENGINE.enableVideo(); + /**Set up to play remote sound with receiver*/ + ENGINE.setDefaultAudioRoutetoSpeakerphone(false); + ENGINE.setEnableSpeakerphone(false); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = ENGINE.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + private void bindVideoService() { + Intent intent = new Intent(); + intent.setClass(getContext(), ExternalVideoInputService.class); + mServiceConnection = new VideoInputServiceConnection(); + getContext().bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); + } + + private void unbindVideoService() { + if (mServiceConnection != null) { + getContext().unbindService(mServiceConnection); + mServiceConnection = null; + } + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + ENGINE.uploadLogFile(); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(() -> { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + screenShare.setEnabled(true); + localVideo.setEnabled(mLocalVideoExists); + bindVideoService(); + }); + } + + /**Since v2.9.0. + * Occurs when the remote video state changes. + * PS: This callback does not work properly when the number of users (in the Communication + * profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the remote user whose video state changes. + * @param state State of the remote video: + * REMOTE_VIDEO_STATE_STOPPED(0): The remote video is in the default state, probably due + * to REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3), REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5), + * or REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7). + * REMOTE_VIDEO_STATE_STARTING(1): The first remote video packet is received. + * REMOTE_VIDEO_STATE_DECODING(2): The remote video stream is decoded and plays normally, + * probably due to REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY (2), + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4), REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6), + * or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9). + * REMOTE_VIDEO_STATE_FROZEN(3): The remote video is frozen, probably due to + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1) or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8). + * REMOTE_VIDEO_STATE_FAILED(4): The remote video fails to start, probably due to + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0). + * @param reason The reason of the remote video state change: + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0): Internal reasons. + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3): The local user stops receiving the remote + * video stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote + * video stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5): The remote user stops sending the video + * stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the video + * stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8): The remote media stream falls back to the + * audio-only stream due to poor network conditions. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9): The remote media stream switches + * back to the video stream after the network conditions improve. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method until + * the SDK triggers this callback.*/ + @Override + public void onRemoteVideoStateChanged(int uid, int state, int reason, int elapsed) { + super.onRemoteVideoStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteVideoStateChanged:uid->" + uid + ", state->" + state); + if (state == REMOTE_VIDEO_STATE_STARTING) { + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + handler.post(() -> + { + /**Display remote video stream*/ + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + surfaceView.setZOrderMediaOverlay(true); + if (fl_remote.getChildCount() > 0) { + fl_remote.removeAllViews(); + } + fl_remote.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + /**Setup remote video to render*/ + ENGINE.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, uid)); + }); + } + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + handler.post(new Runnable() { + @Override + public void run() { + /**Clear render view + Note: The video will stay at its last frame, to completely remove it you will need to + remove the SurfaceView from its parent*/ + ENGINE.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); + fl_remote.removeAllViews(); + } + }); + } + }; + + private class VideoInputServiceConnection implements ServiceConnection { + @Override + public void onServiceConnected(ComponentName componentName, IBinder iBinder) { + mService = (IExternalVideoInputService) iBinder; + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + mService = null; + } + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VideoMetadata.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VideoMetadata.java new file mode 100644 index 000000000..938048b42 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VideoMetadata.java @@ -0,0 +1,541 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import java.nio.charset.Charset; + +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IMetadataObserver; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static io.agora.api.component.Constant.ENGINE; +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; +import static io.agora.rtc.video.VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15; +import static io.agora.rtc.video.VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; +import static io.agora.rtc.video.VideoEncoderConfiguration.STANDARD_BITRATE; +import static io.agora.rtc.video.VideoEncoderConfiguration.VD_640x360; + +@Example( + index = 11, + group = ADVANCED, + name = R.string.item_videometadata, + actionId = R.id.action_mainFragment_to_VideoMetadata, + tipsId = R.string.videometadata +) +public class VideoMetadata extends BaseFragment implements View.OnClickListener +{ + public static final String TAG = VideoMetadata.class.getSimpleName(); + private FrameLayout fl_local, fl_remote; + private Button send, join; + private EditText et_channel; + private RtcEngine engine; + private int myUid; + private boolean joined = false; + /** + * Maximum length of meta data + */ + private int MAX_META_SIZE = 1024; + /** + * Meta data to be sent + */ + private byte[] metadata; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_video_metadata, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); + send = view.findViewById(R.id.btn_send); + send.setOnClickListener(this); + send.setEnabled(false); + join = view.findViewById(R.id.btn_join); + et_channel = view.findViewById(R.id.et_channel); + view.findViewById(R.id.btn_join).setOnClickListener(this); + fl_local = view.findViewById(R.id.fl_local); + fl_remote = view.findViewById(R.id.fl_remote); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) + { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + try + { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); + } + catch (Exception e) + { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() + { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if (engine != null) + { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + } + + @Override + public void onClick(View v) + { + if (v.getId() == R.id.btn_join) + { + if (!joined) + { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) + { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE, + Permission.Group.CAMERA + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } + else + { + joined = false; + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + send.setEnabled(false); + join.setText(getString(R.string.join)); + } + } + else if (v.getId() == R.id.btn_send) + { + /**Click once, the metadata is sent once. + * {@link VideoMetadata#iMetadataObserver}. + * The metadata here can be flexibly replaced according to your own business.*/ + metadata = String.valueOf(System.currentTimeMillis()).getBytes(Charset.forName("UTF-8")); + } + } + + private void joinChannel(String channelId) + { + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + + // Create render view by RtcEngine + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + // Add to the local container + fl_local.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup local video to render your local camera preview + engine.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0)); + + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + // Enable video module + engine.enableVideo(); + // Setup video encoding configs + engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + ((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(), + VideoEncoderConfiguration.FRAME_RATE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate()), + STANDARD_BITRATE, + VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) + )); + /**Set up to play remote sound with receiver*/ + engine.setDefaultAudioRoutetoSpeakerphone(false); + engine.setEnableSpeakerphone(false); + + /**register metadata observer + * @return 0:Success + * < 0:Failure*/ + int code = engine.registerMediaMetadataObserver(iMetadataObserver, IMetadataObserver.VIDEO_METADATA); + Log.e(TAG, code + ""); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) + { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) + { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + /** + * By implementing this interface, metadata can be sent and received with video frames. + */ + private final IMetadataObserver iMetadataObserver = new IMetadataObserver() + { + /**Returns the maximum data size of Metadata*/ + @Override + public int getMaxMetadataSize() + { + return MAX_META_SIZE; + } + + /**Occurs when the SDK is ready to receive and send metadata. + * You need to specify the metadata in the return value of this callback. + * @param timeStampMs The timestamp (ms) of the current metadata. + * @return The metadata that you want to send in the format of byte[]. Ensure that you set the return value. + * PS: Ensure that the size of the metadata does not exceed the value set in the getMaxMetadataSize callback.*/ + @Override + public byte[] onReadyToSendMetadata(long timeStampMs) + { + /**Check if the metadata is empty.*/ + if (metadata == null) + { + return null; + } + Log.i(TAG, "There is metadata to send!"); + /**Recycle metadata objects.*/ + byte[] toBeSend = metadata; + metadata = null; + if (toBeSend.length > MAX_META_SIZE) + { + Log.e(TAG, String.format("Metadata exceeding max length %d!", MAX_META_SIZE)); + return null; + } + String data = new String(toBeSend, Charset.forName("UTF-8")); + handler.post(new Runnable() { + @Override + public void run() { + Toast.makeText(getContext(), String.format(getString(R.string.sent), data), 300).show(); + } + }); + Log.i(TAG, String.format("Metadata sent successfully! The content is %s", data)); + return toBeSend; + } + + /**Occurs when the local user receives the metadata. + * @param buffer The received metadata. + * @param uid The ID of the user who sent the metadata. + * @param timeStampMs The timestamp (ms) of the received metadata.*/ + @Override + public void onMetadataReceived(byte[] buffer, int uid, long timeStampMs) + { + String data = new String(buffer, Charset.forName("UTF-8")); + handler.post(new Runnable() { + @Override + public void run() { + Toast.makeText(getContext(), String.format(getString(R.string.received), data), 300).show(); + } + }); + Log.i(TAG, "onMetadataReceived:" + data); + } + }; + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() + { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) + { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) + { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) + { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) + { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() + { + @Override + public void run() + { + send.setEnabled(true); + join.setEnabled(true); + join.setText(getString(R.string.leave)); + } + }); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Since v2.9.0. + * Occurs when the remote video state changes. + * PS: This callback does not work properly when the number of users (in the Communication + * profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the remote user whose video state changes. + * @param state State of the remote video: + * REMOTE_VIDEO_STATE_STOPPED(0): The remote video is in the default state, probably due + * to REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3), REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5), + * or REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7). + * REMOTE_VIDEO_STATE_STARTING(1): The first remote video packet is received. + * REMOTE_VIDEO_STATE_DECODING(2): The remote video stream is decoded and plays normally, + * probably due to REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY (2), + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4), REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6), + * or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9). + * REMOTE_VIDEO_STATE_FROZEN(3): The remote video is frozen, probably due to + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1) or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8). + * REMOTE_VIDEO_STATE_FAILED(4): The remote video fails to start, probably due to + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0). + * @param reason The reason of the remote video state change: + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0): Internal reasons. + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3): The local user stops receiving the remote + * video stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote + * video stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5): The remote user stops sending the video + * stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the video + * stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8): The remote media stream falls back to the + * audio-only stream due to poor network conditions. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9): The remote media stream switches + * back to the video stream after the network conditions improve. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method until + * the SDK triggers this callback.*/ + @Override + public void onRemoteVideoStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteVideoStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteVideoStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) + { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + handler.post(() -> + { + /**Display remote video stream*/ + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + surfaceView.setZOrderMediaOverlay(true); + if (fl_remote.getChildCount() > 0) + { + fl_remote.removeAllViews(); + } + // Add to the remote container + fl_remote.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + // Setup remote video to render + engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, uid)); + }); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) + { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + handler.post(new Runnable() + { + @Override + public void run() + { + /**Clear render view + Note: The video will stay at its last frame, to completely remove it you will need to + remove the SurfaceView from its parent*/ + engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); + } + }); + } + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VideoQuickSwitch.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VideoQuickSwitch.java new file mode 100644 index 000000000..656e09233 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VideoQuickSwitch.java @@ -0,0 +1,630 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import java.util.ArrayList; +import java.util.List; + +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.rtc.Constants.REMOTE_VIDEO_STATE_DECODING; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; +import static io.agora.rtc.video.VideoEncoderConfiguration.STANDARD_BITRATE; + +/**---------------------------------------Important!!!---------------------------------------------- + * This example demonstrates how audience can quickly switch channels. The following points need to be noted: + 1: You can only access the channel as an audience{@link VideoQuickSwitch#joinChannel(String)}. + 2: If you want to see a normal remote screen, you need to set up several live rooms in advance and + push the stream as a live one (the name of the live room is in the channels instance{"channel0", "channel1", "channel2"}; + at the same time, the appid you used to set up the live room should be consistent with this example program). + * @author cjw*/ +@Example( + index = 2, + group = ADVANCED, + name = R.string.item_quickswitck, + actionId = R.id.action_mainFragment_to_QuickSwitch, + tipsId = R.string.quickswitchchannel +) +public class VideoQuickSwitch extends BaseFragment +{ + private static final String TAG = VideoQuickSwitch.class.getSimpleName(); + private ViewPager viewPager; + private RtcEngine engine; + private int myUid; + private final String[] channels = new String[]{"channel0", "channel1", "channel2"}; + private List channelList = new ArrayList<>(); + private ViewPagerAdapter viewPagerAdapter; + private int currentIndex = 0; + private int lastIndex = -1; + private boolean noBroadcaster = true; + private Runnable runnable = new Runnable() + { + @Override + public void run() + { + if(noBroadcaster) + { + /**There is no broadcaster in the current channel*/ + viewPagerAdapter.notifyBroadcaster(currentIndex, !noBroadcaster); + } + } + }; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + try + { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); + } + catch (Exception e) + { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_quick_switch_channel, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); + viewPager = view.findViewById(R.id.viewPager); + /**Prepare data*/ + for (String channel : channels) + { + channelList.add(channel); + } + viewPagerAdapter = new ViewPagerAdapter(getContext(), channelList); + viewPager.setAdapter(viewPagerAdapter); + viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() + { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) + { + if (positionOffset == 0f && position != currentIndex) + { + viewPager.post(new Runnable() + { + @Override + public void run() + { + Log.i(TAG, "Will switch channel to " + channelList.get(position)); + + currentIndex = position; + if (lastIndex >= 0) + { + viewPagerAdapter.removeSurfaceViewByIndex(lastIndex); + viewPagerAdapter.notifyBroadcaster(lastIndex, true); + } + + /**Since v2.9.0. + * Switches to a different channel. + * This method allows the audience of a Live-broadcast channel to switch to a different channel. + * After the user successfully switches to another channel, the onLeaveChannel + * and onJoinChannelSuccess callbacks are triggered to indicate that the + * user has left the original channel and joined a new one. + * @param token The token for authentication: + * In situations not requiring high security: You can use + * the temporary token generated at Console. For details, + * see Get a temporary token. + * In situations requiring high security: Set it as the token + * generated at your server. For details, see Get a token. + * @param channelName Unique channel name for the AgoraRTC session in the + * string format. The string length must be less than + * 64 bytes. Supported character scopes are: + * All lowercase English letters: a to z. + * All uppercase English letters: A to Z. + * All numeric characters: 0 to 9. + * The space character. + * Punctuation characters and other symbols, including: + * "!", "#", "$", "%", "&", "(", ")", "+", "-", ":", + * ";", "<", "=", ".", ">", "?", "@", "[", "]", "^", + * "_", " {", "}", "|", "~", ",". + * @return + * 0: Success. + * < 0: Failure. + * PS: + * Important!!!This method applies to the audience role in a + * Live-broadcast channel only.*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int code = engine.switchChannel(null, channelList.get(position), option); + + lastIndex = currentIndex; + } + }); + } + } + @Override + public void onPageSelected(int position) + {} + @Override + public void onPageScrollStateChanged(int state) + {} + }); + /**Swipe left and right to switch channel tips*/ + showAlert(getString(R.string.swiptips)); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) + { + super.onActivityCreated(savedInstanceState); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) + { + joinChannel(channelList.get(0)); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.READ_EXTERNAL_STORAGE, + Permission.WRITE_EXTERNAL_STORAGE + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelList.get(0)); + }).start(); + } + + @Override + public void onDestroy() + { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if (engine != null) + { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + } + + private final void joinChannel(String channelId) + { + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + + // Set audio route to speaker + engine.setDefaultAudioRoutetoSpeakerphone(true); + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the broadcaster.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + // Enable video module + engine.enableVideo(); + // Setup video encoding configs + engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + ((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(), + VideoEncoderConfiguration.FRAME_RATE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate()), + STANDARD_BITRATE, + VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) + )); + /**Set up to play remote sound with receiver*/ + engine.setDefaultAudioRoutetoSpeakerphone(false); + engine.setEnableSpeakerphone(false); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) + { + accessToken = null; + } + + /**Allows a user to join a channel. + * if you do not specify the uid, we will generate the uid for you. + * If your account has enabled token mechanism through the console, you must fill in the + * corresponding token here. In general, it is not recommended to open the token mechanism in the test phase.*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) + { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() + { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) + { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) + { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics. + * PS: + * Important! Because the channel is entered by the role of an audience, this callback will + * only be received when the broadcaster exits the channel.*/ + @Override + public void onLeaveChannel(RtcStats stats) + { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + handler.removeCallbacks(runnable); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) + { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + /**Determine if there is a host in the channel*/ + noBroadcaster = true; + handler.post(runnable); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Since v2.9.0. + * Occurs when the remote video state changes. + * PS: This callback does not work properly when the number of users (in the Communication + * profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the remote user whose video state changes. + * @param state State of the remote video: + * REMOTE_VIDEO_STATE_STOPPED(0): The remote video is in the default state, probably due + * to REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3), REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5), + * or REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7). + * REMOTE_VIDEO_STATE_STARTING(1): The first remote video packet is received. + * REMOTE_VIDEO_STATE_DECODING(2): The remote video stream is decoded and plays normally, + * probably due to REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY (2), + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4), REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6), + * or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9). + * REMOTE_VIDEO_STATE_FROZEN(3): The remote video is frozen, probably due to + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1) or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8). + * REMOTE_VIDEO_STATE_FAILED(4): The remote video fails to start, probably due to + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0). + * @param reason The reason of the remote video state change: + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0): Internal reasons. + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3): The local user stops receiving the remote + * video stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote + * video stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5): The remote user stops sending the video + * stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the video + * stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8): The remote media stream falls back to the + * audio-only stream due to poor network conditions. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9): The remote media stream switches + * back to the video stream after the network conditions improve. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method until + * the SDK triggers this callback.*/ + @Override + public void onRemoteVideoStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteVideoStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteVideoStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + if(state == REMOTE_VIDEO_STATE_DECODING) + { + /**REMOTE_VIDEO_STATE_DECODING as the basis for judging whether there is a broadcaster + * in the channel. + * But you should judge according to your own business logic, here is just for example, + * not for reference.*/ + noBroadcaster = false; + } + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered. + * PS: + * Important! Because the channel is entered by the role of an audience, this callback will + * only be received when the broadcaster exits the channel.*/ + @Override + public void onUserJoined(int uid, int elapsed) + { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + handler.post(() -> + { + if(uid != myUid) + { + SurfaceView surfaceV = RtcEngine.CreateRendererView(getContext().getApplicationContext()); + surfaceV.setZOrderMediaOverlay(true); + engine.setupRemoteVideo(new VideoCanvas(surfaceV, VideoCanvas.RENDER_MODE_HIDDEN, uid)); + viewPagerAdapter.setSurfaceView(currentIndex, uid, surfaceV); + } + }); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience. + * PS: + * Important! Because the channel is entered by the role of an audience, this callback will + * only be received when the broadcaster exits the channel.*/ + @Override + public void onUserOffline(int uid, int reason) + { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + handler.post(new Runnable() + { + @Override + public void run() + { + /**Clear render view + Note: The video will stay at its last frame, to completely remove it you will need to + remove the SurfaceView from its parent*/ + engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); + viewPagerAdapter.removeSurfaceView(uid); + } + }); + } + }; + + public class ViewPagerAdapter extends PagerAdapter + { + private SparseArray viewList = new SparseArray<>(); + private Context context; + private List roomNameList = new ArrayList<>(); + + public ViewPagerAdapter(Context context, List roomNameList) + { + this.context = context; + this.roomNameList = roomNameList; + } + + @Override + public Object instantiateItem(ViewGroup collection, int position) + { + ViewGroup layout = viewList.get(position); + if (layout == null) + { + LayoutInflater inflater = LayoutInflater.from(context); + layout = (ViewGroup) inflater.inflate(R.layout.view_item_quickswitch, collection, false); + viewList.put(position, layout); + + TextView channel = layout.findViewById(R.id.channelName); + channel.setText(String.format(getString(R.string.channelstr), roomNameList.get(position))); + } + + collection.addView(layout); + + return layout; + } + + @Override + public void destroyItem(ViewGroup collection, int position, Object view) + { + collection.removeView((View) view); + } + + @Override + public int getCount() + { + return roomNameList.size(); + } + + private void setSurfaceView(int position, final int uid, final SurfaceView view) + { + final ViewGroup viewGroup = viewList.get(position); + if (viewGroup != null) + { + ViewGroup surfaceContainer = viewGroup.findViewById(R.id.fl_remote); + surfaceContainer.removeAllViews(); + view.setZOrderMediaOverlay(true); + surfaceContainer.addView(view, new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + TextView uidTextView = viewGroup.findViewById(R.id.uid); + uidTextView.setText(String.format("Uid: %d", uid)); + + viewGroup.setTag(uid); + } + } + + private void removeSurfaceView(int uid) + { + for (int i = 0; i < viewList.size(); i++) + { + ViewGroup viewGroup = viewList.get(i); + + if (viewGroup.getTag() != null && ((Integer) viewGroup.getTag()) == uid) + { + removeSurfaceView(viewGroup); + } + } + } + + private void removeSurfaceViewByIndex(int index) + { + ViewGroup viewGroup = viewList.get(index); + if (viewGroup != null) + { + removeSurfaceView(viewGroup); + } + } + + private void removeSurfaceView(ViewGroup viewGroup) + { + ViewGroup surfaceContainer = viewGroup.findViewById(R.id.fl_remote); + surfaceContainer.removeAllViews(); + + TextView uidTextView = viewGroup.findViewById(R.id.uid); + uidTextView.setText(""); + } + + public void notifyBroadcaster(int index, boolean exists) + { + ViewGroup viewGroup = viewList.get(index); + if (viewGroup != null) + { + TextView textView = viewGroup.findViewById(R.id.noBroadcaster); + textView.setVisibility(exists ? View.GONE : View.VISIBLE); + } + } + + @Override + public boolean isViewFromObject(@NonNull View view, @NonNull Object object) + { + return view == object; + } + + @Override + public CharSequence getPageTitle(int position) + { + return ""; + } + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VoiceEffects.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VoiceEffects.java new file mode 100644 index 000000000..027fb00f1 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/VoiceEffects.java @@ -0,0 +1,569 @@ +package io.agora.api.example.examples.advanced; + +import android.content.Context; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.os.Handler; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.EditText; +import android.widget.PopupWindow; +import android.widget.SeekBar; +import android.widget.Spinner; +import android.widget.Switch; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.models.ChannelMediaOptions; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.rtc.Constants.*; + +@Example( + index = 15, + group = ADVANCED, + name = R.string.item_voiceeffects, + actionId = R.id.action_mainFragment_to_VoiceEffects, + tipsId = R.string.voiceeffects +) +public class VoiceEffects extends BaseFragment implements View.OnClickListener, AdapterView.OnItemSelectedListener, CompoundButton.OnCheckedChangeListener { + private static final String TAG = VoiceEffects.class.getSimpleName(); + private EditText et_channel; + private Button join, effectOptions, ok; + private RtcEngine engine; + private int myUid; + private boolean joined = false; + private Spinner preset, beautifier, pitch1, pitch2; + private PopupWindow popupWindow; + private Switch effectOption; + private SeekBar voiceCircle; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + handler = new Handler(); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_voice_effects, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + et_channel = view.findViewById(R.id.et_channel); + view.findViewById(R.id.btn_join).setOnClickListener(this); + preset = view.findViewById(R.id.audio_preset_spinner); + beautifier = view.findViewById(R.id.voice_beautifier_spinner); + preset.setOnItemSelectedListener(this); + beautifier.setOnItemSelectedListener(this); + effectOptions = view.findViewById(R.id.btn_effect_options); + effectOptions.setOnClickListener(this); + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View vPopupWindow = inflater.inflate(R.layout.popup_effect_options, null, false); + popupWindow = new PopupWindow(vPopupWindow, + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, true); + popupWindow.setBackgroundDrawable(new ColorDrawable(0xefefefef)); + ok = vPopupWindow.findViewById(R.id.btn_ok); + ok.setOnClickListener(this); + pitch1 = vPopupWindow.findViewById(R.id.pitch_option1); + pitch2 = vPopupWindow.findViewById(R.id.pitch_option2); + effectOption = vPopupWindow.findViewById(R.id.switch_effect_option); + effectOption.setOnCheckedChangeListener(this); + voiceCircle = vPopupWindow.findViewById(R.id.room_acoustics_3d_voice); + toggleEffectOptionsDisplay(false); + effectOptions.setEnabled(false); + preset.setEnabled(false); + beautifier.setEnabled(false); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) + { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + try + { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + String appId = getString(R.string.agora_app_id); + engine = RtcEngine.create(getContext().getApplicationContext(), appId, iRtcEngineEventHandler); + } + catch (Exception e) + { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() + { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if(engine != null) + { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + } + + @Override + public void onClick(View v) + { + if (v.getId() == R.id.btn_join) + { + if (!joined) + { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) + { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } + else + { + joined = false; + preset.setEnabled(false); + beautifier.setEnabled(false); + effectOptions.setEnabled(false); + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + join.setText(getString(R.string.join)); + } + } + else if(v.getId() == R.id.btn_effect_options){ + popupWindow.showAsDropDown(v, 50, 0); + } + else if(v.getId() == R.id.btn_ok){ + boolean isPitch = effectOption.isChecked(); + if(isPitch){ + int effectOption1 = getPitch1Value(pitch1.getSelectedItem().toString()); + int effectOption2 = getPitch2Value(pitch2.getSelectedItem().toString()); + engine.setAudioEffectParameters(PITCH_CORRECTION, effectOption1, effectOption2); + } + else{ + int voiceCircleOption = voiceCircle.getProgress(); + engine.setAudioEffectParameters(ROOM_ACOUSTICS_3D_VOICE, voiceCircleOption, 0); + } + popupWindow.dismiss(); + } + } + + private int getPitch1Value(String str) { + switch (str){ + case "Natural Minor": + return 2; + case "Breeze Minor": + return 3; + default: + return 1; + } + } + + private int getPitch2Value(String str) { + switch (str){ + case "A Pitch": + return 1; + case "A# Pitch": + return 2; + case "B Pitch": + return 3; + case "C# Pitch": + return 5; + case "D Pitch": + return 6; + case "D# Pitch": + return 7; + case "E Pitch": + return 8; + case "F Pitch": + return 9; + case "F# Pitch": + return 10; + case "G Pitch": + return 11; + case "G# Pitch": + return 12; + default: + return 4; + } + } + + /** + * @param channelId Specify the channel name that you want to join. + * Users that input the same channel name join the same channel.*/ + private void joinChannel(String channelId) + { + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) + { + accessToken = null; + } + + engine.setAudioProfile(AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO, AUDIO_SCENARIO_GAME_STREAMING); + + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) + { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + /**IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() + { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) + { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) + { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) + { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) + { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(() -> { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + preset.setEnabled(true); + beautifier.setEnabled(true); + effectOptions.setEnabled(true); + }); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) + { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) + { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + } + }; + + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + if(parent.getId() == R.id.audio_preset_spinner){ + String item = preset.getSelectedItem().toString(); + engine.setAudioEffectPreset(getAudioEffectPreset(item)); + } + else if(parent.getId() == R.id.voice_beautifier_spinner){ + String item = beautifier.getSelectedItem().toString(); + engine.setVoiceBeautifierPreset(getVoiceBeautifierValue(item)); + } + } + + private int getVoiceBeautifierValue(String label) { + int value; + switch (label) { + case "CHAT_BEAUTIFIER_MAGNETIC": + value = CHAT_BEAUTIFIER_MAGNETIC; + break; + case "CHAT_BEAUTIFIER_FRESH": + value = CHAT_BEAUTIFIER_FRESH; + break; + case "CHAT_BEAUTIFIER_VITALITY": + value = CHAT_BEAUTIFIER_VITALITY; + break; + case "TIMBRE_TRANSFORMATION_VIGOROUS": + value = TIMBRE_TRANSFORMATION_VIGOROUS; + break; + case "TIMBRE_TRANSFORMATION_DEEP": + value = TIMBRE_TRANSFORMATION_DEEP; + break; + case "TIMBRE_TRANSFORMATION_MELLOW": + value = TIMBRE_TRANSFORMATION_MELLOW; + break; + case "TIMBRE_TRANSFORMATION_FALSETTO": + value = TIMBRE_TRANSFORMATION_FALSETTO; + break; + case "TIMBRE_TRANSFORMATION_FULL": + value = TIMBRE_TRANSFORMATION_FULL; + break; + case "TIMBRE_TRANSFORMATION_CLEAR": + value = TIMBRE_TRANSFORMATION_CLEAR; + break; + case "TIMBRE_TRANSFORMATION_RESOUNDING": + value = TIMBRE_TRANSFORMATION_RESOUNDING; + break; + case "TIMBRE_TRANSFORMATION_RINGING": + value = TIMBRE_TRANSFORMATION_RINGING; + break; + default: + value = VOICE_BEAUTIFIER_OFF; + } + return value; + } + + private int getAudioEffectPreset(String label){ + int value; + switch (label){ + case "ROOM_ACOUSTICS_KTV": + value = ROOM_ACOUSTICS_KTV; + break; + case "ROOM_ACOUSTICS_VOCAL_CONCERT": + value = ROOM_ACOUSTICS_VOCAL_CONCERT; + break; + case "ROOM_ACOUSTICS_STUDIO": + value = ROOM_ACOUSTICS_STUDIO; + break; + case "ROOM_ACOUSTICS_PHONOGRAPH": + value = ROOM_ACOUSTICS_PHONOGRAPH; + break; + case "ROOM_ACOUSTICS_VIRTUAL_STEREO": + value = ROOM_ACOUSTICS_VIRTUAL_STEREO; + break; + case "ROOM_ACOUSTICS_SPACIAL": + value = ROOM_ACOUSTICS_SPACIAL; + break; + case "ROOM_ACOUSTICS_ETHEREAL": + value = ROOM_ACOUSTICS_ETHEREAL; + break; + case "ROOM_ACOUSTICS_3D_VOICE": + value = ROOM_ACOUSTICS_3D_VOICE; + break; + case "VOICE_CHANGER_EFFECT_UNCLE": + value = VOICE_CHANGER_EFFECT_UNCLE; + break; + case "VOICE_CHANGER_EFFECT_OLDMAN": + value = VOICE_CHANGER_EFFECT_OLDMAN; + break; + case "VOICE_CHANGER_EFFECT_BOY": + value = VOICE_CHANGER_EFFECT_BOY; + break; + case "VOICE_CHANGER_EFFECT_SISTER": + value = VOICE_CHANGER_EFFECT_SISTER; + break; + case "VOICE_CHANGER_EFFECT_GIRL": + value = VOICE_CHANGER_EFFECT_GIRL; + break; + case "VOICE_CHANGER_EFFECT_PIGKING": + value = VOICE_CHANGER_EFFECT_PIGKING; + break; + case "VOICE_CHANGER_EFFECT_HULK": + value = VOICE_CHANGER_EFFECT_HULK; + break; + case "STYLE_TRANSFORMATION_RNB": + value = STYLE_TRANSFORMATION_RNB; + break; + case "STYLE_TRANSFORMATION_POPULAR": + value = STYLE_TRANSFORMATION_POPULAR; + break; + case "PITCH_CORRECTION": + value = PITCH_CORRECTION; + break; + default: + value = AUDIO_EFFECT_OFF; + } + return value; + } + + + @Override + public void onNothingSelected(AdapterView parent) { + + } + + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + toggleEffectOptionsDisplay(isChecked); + } + + private void toggleEffectOptionsDisplay(boolean isChecked){ + pitch1.setVisibility(isChecked?View.VISIBLE:View.GONE); + pitch2.setVisibility(isChecked?View.VISIBLE:View.GONE); + voiceCircle.setVisibility(isChecked?View.GONE:View.VISIBLE); + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/AudioPlayer.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/AudioPlayer.java new file mode 100644 index 000000000..7074c2727 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/AudioPlayer.java @@ -0,0 +1,67 @@ +package io.agora.api.example.examples.advanced.customaudio; + +import android.media.AudioFormat; +import android.media.AudioTrack; +import android.util.Log; + +public class AudioPlayer { + + private static final int DEFAULT_PLAY_MODE = AudioTrack.MODE_STREAM; + private static final String TAG = "AudioPlayer"; + + private AudioTrack mAudioTrack; + private AudioStatus mAudioStatus = AudioStatus.STOPPED ; + + public AudioPlayer(int streamType, int sampleRateInHz, int channelConfig, int audioFormat){ + if(mAudioStatus == AudioStatus.STOPPED) { + int Val = 0; + if(1 == channelConfig) + Val = AudioFormat.CHANNEL_OUT_MONO; + else if(2 == channelConfig) + Val = AudioFormat.CHANNEL_OUT_STEREO; + else + Log.e(TAG, "channelConfig is wrong !"); + + int mMinBufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, Val, audioFormat); + Log.e(TAG, " sampleRateInHz :" + sampleRateInHz + " channelConfig :" + channelConfig + " audioFormat: " + audioFormat + " mMinBufferSize: " + mMinBufferSize); + if (mMinBufferSize == AudioTrack.ERROR_BAD_VALUE) { + Log.e(TAG,"AudioTrack.ERROR_BAD_VALUE : " + AudioTrack.ERROR_BAD_VALUE) ; + } + + mAudioTrack = new AudioTrack(streamType, sampleRateInHz, Val, audioFormat, mMinBufferSize, DEFAULT_PLAY_MODE); + if (mAudioTrack.getState() == AudioTrack.STATE_UNINITIALIZED) { + throw new RuntimeException("Error on AudioTrack created"); + } + mAudioStatus = AudioStatus.INITIALISING; + } + Log.e(TAG, "mAudioStatus: " + mAudioStatus); + } + + public boolean startPlayer() { + if(mAudioStatus == AudioStatus.INITIALISING) { + mAudioTrack.play(); + mAudioStatus = AudioStatus.RUNNING; + } + Log.e("AudioPlayer", "mAudioStatus: " + mAudioStatus); + return true; + } + + public void stopPlayer() { + if(null != mAudioTrack){ + mAudioStatus = AudioStatus.STOPPED; + mAudioTrack.stop(); + mAudioTrack.release(); + mAudioTrack = null; + } + Log.e(TAG, "mAudioStatus: " + mAudioStatus); + } + + public boolean play(byte[] audioData, int offsetInBytes, int sizeInBytes) { + if(mAudioStatus == AudioStatus.RUNNING) { + mAudioTrack.write(audioData, offsetInBytes, sizeInBytes); + }else{ + Log.e(TAG, "=== No data to AudioTrack !! mAudioStatus: " + mAudioStatus); + } + return true; + } +} \ No newline at end of file diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/AudioRecordService.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/AudioRecordService.java new file mode 100644 index 000000000..0b32f2b6a --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/AudioRecordService.java @@ -0,0 +1,173 @@ +package io.agora.api.example.examples.advanced.customaudio; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.media.AudioFormat; +import android.media.AudioRecord; +import android.media.MediaRecorder; +import android.os.Build; +import android.os.IBinder; +import android.util.Log; + +import androidx.annotation.Nullable; +import androidx.core.app.NotificationCompat; + +import io.agora.api.example.MainActivity; + +public class AudioRecordService extends Service +{ + private static final String TAG = AudioRecordService.class.getSimpleName(); + + private RecordThread thread; + private volatile boolean stopped; + + @Nullable + @Override + public IBinder onBind(Intent intent) + { + return null; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) + { + startForeground(); + startRecording(); + return Service.START_STICKY; + } + + private void startForeground() + { + createNotificationChannel(); + Intent notificationIntent = new Intent(this, MainActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity(this, + 0, notificationIntent, 0); + + Notification notification = new NotificationCompat.Builder(this, TAG) + .setContentTitle(TAG) + .setContentIntent(pendingIntent) + .build(); + + startForeground(1, notification); + } + + private void createNotificationChannel() + { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + { + NotificationChannel serviceChannel = new NotificationChannel( + TAG, TAG, NotificationManager.IMPORTANCE_DEFAULT + ); + + NotificationManager manager = getSystemService(NotificationManager.class); + manager.createNotificationChannel(serviceChannel); + } + } + + private void startRecording() + { + thread = new RecordThread(); + thread.start(); + } + + private void stopRecording() + { + stopped = true; + } + + @Override + public void onDestroy() + { + stopRecording(); + super.onDestroy(); + } + + public class RecordThread extends Thread + { + private AudioRecord audioRecord; + public static final int DEFAULT_SAMPLE_RATE = 16000; + /**1 corresponds to AudioFormat.CHANNEL_IN_MONO; + * 2 corresponds to AudioFormat.CHANNEL_IN_STEREO*/ + public static final int DEFAULT_CHANNEL_COUNT = 1, DEFAULT_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO; + private byte[] buffer; + + RecordThread() + { + int bufferSize = AudioRecord.getMinBufferSize(DEFAULT_SAMPLE_RATE, DEFAULT_CHANNEL_CONFIG, + AudioFormat.ENCODING_PCM_16BIT); + audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, DEFAULT_SAMPLE_RATE, DEFAULT_CHANNEL_COUNT, + AudioFormat.ENCODING_PCM_16BIT, bufferSize); + buffer = new byte[bufferSize]; + } + + @Override + public void run() + { + try + { + audioRecord.startRecording(); + while (!stopped) + { + int result = audioRecord.read(buffer, 0, buffer.length); + if (result >= 0) + { + /**Pushes the external audio frame to the Agora SDK for encoding. + * @param data External audio data to be pushed. + * @param timeStamp Timestamp of the external audio frame. It is mandatory. + * You can use this parameter for the following purposes: + * 1:Restore the order of the captured audio frame. + * 2:Synchronize audio and video frames in video-related + * scenarios, including scenarios where external video sources are used. + * @return + * 0: Success. + * < 0: Failure.*/ + CustomAudioSource.engine.pushExternalAudioFrame( + buffer, System.currentTimeMillis()); + } + else + { + logRecordError(result); + } + Log.e(TAG, "数据大小:" + result); + } + release(); + } + catch (Exception e) + {e.printStackTrace();} + } + + private void logRecordError(int error) + { + String message = ""; + switch (error) + { + case AudioRecord.ERROR: + message = "generic operation failure"; + break; + case AudioRecord.ERROR_BAD_VALUE: + message = "failure due to the use of an invalid value"; + break; + case AudioRecord.ERROR_DEAD_OBJECT: + message = "object is no longer valid and needs to be recreated"; + break; + case AudioRecord.ERROR_INVALID_OPERATION: + message = "failure due to the improper use of method"; + break; + } + Log.e(TAG, message); + } + + private void release() + { + if (audioRecord != null) + { + audioRecord.stop(); + buffer = null; + } + } + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/AudioStatus.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/AudioStatus.java new file mode 100644 index 000000000..ae71019c3 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/AudioStatus.java @@ -0,0 +1,7 @@ +package io.agora.api.example.examples.advanced.customaudio; + +public enum AudioStatus { + INITIALISING, + RUNNING, + STOPPED +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioSource.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioSource.java new file mode 100755 index 000000000..fb0d634fe --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customaudio/CustomAudioSource.java @@ -0,0 +1,388 @@ +package io.agora.api.example.examples.advanced.customaudio; + +import android.content.Context; +import android.content.Intent; +import android.media.AudioFormat; +import android.media.AudioManager; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.models.ChannelMediaOptions; + +import static io.agora.api.example.common.model.Examples.ADVANCED; +import static io.agora.api.example.examples.advanced.customaudio.AudioRecordService.RecordThread.DEFAULT_CHANNEL_COUNT; +import static io.agora.api.example.examples.advanced.customaudio.AudioRecordService.RecordThread.DEFAULT_SAMPLE_RATE; + +/**This demo demonstrates how to make a one-to-one voice call*/ +@Example( + index = 8, + group = ADVANCED, + name = R.string.item_customaudiosource, + actionId = R.id.action_mainFragment_to_CustomAudioSource, + tipsId = R.string.customaudio +) +public class CustomAudioSource extends BaseFragment implements View.OnClickListener +{ + private static final String TAG = CustomAudioSource.class.getSimpleName(); + private EditText et_channel; + private Button mute, join; + private int myUid; + private boolean joined = false; + public static RtcEngine engine; + private static final Integer SAMPLE_RATE = 44100; + private static final Integer SAMPLE_NUM_OF_CHANNEL = 1; + private AudioPlayer mAudioPlayer; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + handler = new Handler(); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_custom_audiorecord, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + et_channel = view.findViewById(R.id.et_channel); + view.findViewById(R.id.btn_join).setOnClickListener(this); + mute = view.findViewById(R.id.btn_mute); + mute.setOnClickListener(this); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) + { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + try + { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + engine = RtcEngine.create(getContext().getApplicationContext(), getString(R.string.agora_app_id), + iRtcEngineEventHandler); + + // Notify the SDK that you want to use the external audio sink. + engine.setExternalAudioSink( + true, // Enable the external audio sink. + SAMPLE_RATE, // Set the audio sample rate as 8k, 16k, 32k, 44.1k or 48kHz. + SAMPLE_NUM_OF_CHANNEL // Number of channels. The maximum number is 2. + ); + mAudioPlayer = new AudioPlayer(AudioManager.STREAM_VOICE_CALL, SAMPLE_RATE, SAMPLE_NUM_OF_CHANNEL, AudioFormat.CHANNEL_OUT_MONO); + } + catch (Exception e) + { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() + { + super.onDestroy(); + stopAudioRecord(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if(engine != null) + { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + mAudioPlayer.stopPlayer(); + playerTask.cancel(true); + } + + @Override + public void onClick(View v) + { + if (v.getId() == R.id.btn_join) + { + if (!joined) + { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) + { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } + else + { + joined = false; + stopAudioRecord(); + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + join.setText(getString(R.string.join)); + mute.setText(getString(R.string.closemicrophone)); + mute.setEnabled(false); + } + } + else if (v.getId() == R.id.btn_mute) + { + mute.setActivated(!mute.isActivated()); + mute.setText(getString(mute.isActivated() ? R.string.openmicrophone : R.string.closemicrophone)); + /**Turn off / on the microphone, stop / start local audio collection and push streaming.*/ + engine.muteLocalAudioStream(mute.isActivated()); + } + } + + /** + * @param channelId Specify the channel name that you want to join. + * Users that input the same channel name join the same channel.*/ + private void joinChannel(String channelId) + { + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + /**Sets the external audio source. + * @param enabled Sets whether to enable/disable the external audio source: + * true: Enable the external audio source. + * false: (Default) Disable the external audio source. + * @param sampleRate Sets the sample rate (Hz) of the external audio source, which can be + * set as 8000, 16000, 32000, 44100, or 48000 Hz. + * @param channels Sets the number of channels of the external audio source: + * 1: Mono. + * 2: Stereo. + * @return + * 0: Success. + * < 0: Failure. + * PS: Ensure that you call this method before the joinChannel method.*/ + engine.setExternalAudioSource(true, DEFAULT_SAMPLE_RATE, DEFAULT_CHANNEL_COUNT); + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) + { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) + { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + private void startAudioRecord() + { + Intent intent = new Intent(getContext(), AudioRecordService.class); + getActivity().startService(intent); + } + + private void stopAudioRecord() + { + Intent intent = new Intent(getContext(), AudioRecordService.class); + getActivity().stopService(intent); + } + + private final AsyncTask playerTask = new AsyncTask() { + @Override + protected Object doInBackground(Object[] objects) { + while (true) { + if (engine != null) { + int length = SAMPLE_RATE / 1000 * 2 * SAMPLE_NUM_OF_CHANNEL * 10; + byte[] data = new byte[length]; + /** + * Pulls the remote audio frame. + * Before calling this method, call the setExternalAudioSink(enabled: true) method to enable and set the external audio sink. + * After a successful method call, the app pulls the decoded and mixed audio data for playback. + * @Param data: The audio data that you want to pull. The data format is in byte[]. + * @Param lengthInByte: The data length (byte) of the external audio data. The value of this parameter is related to the audio duration, + * and the values of the sampleRate and channels parameters that you set in setExternalAudioSink. Agora recommends setting the audio duration no shorter than 10 ms. + * The formula for lengthInByte is: + * lengthInByte = sampleRate/1000 × 2 × channels × audio duration (ms). + */ + engine.pullPlaybackAudioFrame(data, length); + mAudioPlayer.play(data, 0, length); + } + } + } + }; + + /**IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() + { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) + { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) + { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) + { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() + { + @Override + public void run() + { + mute.setEnabled(true); + join.setEnabled(true); + join.setText(getString(R.string.leave)); + } + }); + startAudioRecord(); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + mAudioPlayer.startPlayer(); + playerTask.execute(); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/AgoraVideoRender.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/AgoraVideoRender.java new file mode 100644 index 000000000..89fcf25c1 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/AgoraVideoRender.java @@ -0,0 +1,86 @@ +package io.agora.api.example.examples.advanced.customvideo; + +import java.nio.ByteBuffer; + +import io.agora.api.example.common.model.Peer; +import io.agora.rtc.mediaio.IVideoSink; +import io.agora.rtc.mediaio.MediaIO; + +/** + * Created by wyylling@gmail.com on 03/01/2018. + */ + +public class AgoraVideoRender implements IVideoSink { + private Peer mPeer; + private boolean mIsLocal; + + public AgoraVideoRender(int uid, boolean local) { + mPeer = new Peer(); + mPeer.uid = uid; + mIsLocal = local; + } + + public Peer getPeer() { + return mPeer; + } + + @Override + public boolean onInitialize() { + return true; + } + + @Override + public boolean onStart() { + return true; + } + + @Override + public void onStop() { + + } + + @Override + public void onDispose() { + + } + + @Override + public long getEGLContextHandle() { + return 0; + } + + @Override + public int getBufferType() { + return MediaIO.BufferType.BYTE_BUFFER.intValue(); + } + + @Override + public int getPixelFormat() { + return MediaIO.PixelFormat.RGBA.intValue(); + } + + @Override + public void consumeByteBufferFrame(ByteBuffer buffer, int format, int width, int height, int rotation, long ts) { + if (!mIsLocal) { + mPeer.data = buffer; + mPeer.width = width; + mPeer.height = height; + mPeer.rotation = rotation; + mPeer.ts = ts; + } + } + + @Override + public void consumeByteArrayFrame(byte[] data, int format, int width, int height, int rotation, long ts) { + //Log.e("AgoraVideoRender", "consumeByteArrayFrame"); + } + + @Override + public void consumeTextureFrame(int texId, int format, int width, int height, int rotation, long ts, float[] matrix) { + + } + + public interface OnFrameListener { + void consumeByteBufferFrame(int uid, ByteBuffer data, int pixelFormat, int width, int height, int rotation, long ts); + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/AgoraVideoSource.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/AgoraVideoSource.java new file mode 100644 index 000000000..54491374d --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/AgoraVideoSource.java @@ -0,0 +1,51 @@ +package io.agora.api.example.examples.advanced.customvideo; + +import io.agora.rtc.mediaio.IVideoFrameConsumer; +import io.agora.rtc.mediaio.IVideoSource; +import io.agora.rtc.mediaio.MediaIO; + +/** + * Created by wyylling@gmail.com on 03/01/2018. + */ + +public class AgoraVideoSource implements IVideoSource { + private IVideoFrameConsumer mConsumer; + + @Override + public boolean onInitialize(IVideoFrameConsumer iVideoFrameConsumer) { + mConsumer = iVideoFrameConsumer; + return true; + } + + @Override + public boolean onStart() { + return true; + } + + @Override + public void onStop() { + } + + @Override + public void onDispose() { + } + + @Override + public int getBufferType() { + return MediaIO.BufferType.BYTE_ARRAY.intValue(); + } + + @Override + public int getCaptureType() { + return MediaIO.CaptureType.CAMERA.intValue(); + } + + @Override + public int getContentHint() { + return MediaIO.ContentHint.NONE.intValue(); + } + + public IVideoFrameConsumer getConsumer() { + return mConsumer; + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/BackgroundRenderer.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/BackgroundRenderer.java new file mode 100644 index 000000000..41929b642 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/BackgroundRenderer.java @@ -0,0 +1,172 @@ +package io.agora.api.example.examples.advanced.customvideo; + +import android.content.Context; +import android.opengl.GLES11Ext; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; + +import com.google.ar.core.Frame; +import com.google.ar.core.Session; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +import io.agora.api.example.R; + +/** + * This class renders the AR background from camera feed. It creates and hosts the texture + * given to ARCore to be filled with the camera image. + */ +public class BackgroundRenderer { + private static final String TAG = BackgroundRenderer.class.getSimpleName(); + + private static final int COORDS_PER_VERTEX = 3; + private static final int TEXCOORDS_PER_VERTEX = 2; + private static final int FLOAT_SIZE = 4; + + private FloatBuffer mQuadVertices; + private FloatBuffer mQuadTexCoord; + private FloatBuffer mQuadTexCoordTransformed; + + private int mQuadProgram; + + private int mQuadPositionParam; + private int mQuadTexCoordParam; + private int mTextureId = -1; + + public BackgroundRenderer() { + } + + public int getTextureId() { + return mTextureId; + } + + /** + * Allocates and initializes OpenGL resources needed by the background renderer. Must be + * called on the OpenGL thread, typically in + * {@link GLSurfaceView.Renderer#onSurfaceCreated(GL10, EGLConfig)}. + * + * @param context Needed to access shader source. + */ + public void createOnGlThread(Context context) { + // Generate the background texture. + int[] textures = new int[1]; + GLES20.glGenTextures(1, textures, 0); + mTextureId = textures[0]; + int textureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES; + GLES20.glBindTexture(textureTarget, mTextureId); + GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); + GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST); + + int numVertices = 4; + if (numVertices != QUAD_COORDS.length / COORDS_PER_VERTEX) { + throw new RuntimeException("Unexpected number of vertices in BackgroundRenderer."); + } + + ByteBuffer bbVertices = ByteBuffer.allocateDirect(QUAD_COORDS.length * FLOAT_SIZE); + bbVertices.order(ByteOrder.nativeOrder()); + mQuadVertices = bbVertices.asFloatBuffer(); + mQuadVertices.put(QUAD_COORDS); + mQuadVertices.position(0); + + ByteBuffer bbTexCoords = ByteBuffer.allocateDirect( + numVertices * TEXCOORDS_PER_VERTEX * FLOAT_SIZE); + bbTexCoords.order(ByteOrder.nativeOrder()); + mQuadTexCoord = bbTexCoords.asFloatBuffer(); + mQuadTexCoord.put(QUAD_TEXCOORDS); + mQuadTexCoord.position(0); + + ByteBuffer bbTexCoordsTransformed = ByteBuffer.allocateDirect( + numVertices * TEXCOORDS_PER_VERTEX * FLOAT_SIZE); + bbTexCoordsTransformed.order(ByteOrder.nativeOrder()); + mQuadTexCoordTransformed = bbTexCoordsTransformed.asFloatBuffer(); + + int vertexShader = ShaderUtil.loadGLShader(TAG, context, + GLES20.GL_VERTEX_SHADER, R.raw.screenquad_vertex); + int fragmentShader = ShaderUtil.loadGLShader(TAG, context, + GLES20.GL_FRAGMENT_SHADER, R.raw.screenquad_fragment_oes); + + mQuadProgram = GLES20.glCreateProgram(); + GLES20.glAttachShader(mQuadProgram, vertexShader); + GLES20.glAttachShader(mQuadProgram, fragmentShader); + GLES20.glLinkProgram(mQuadProgram); + GLES20.glUseProgram(mQuadProgram); + + ShaderUtil.checkGLError(TAG, "Program creation"); + + mQuadPositionParam = GLES20.glGetAttribLocation(mQuadProgram, "a_Position"); + mQuadTexCoordParam = GLES20.glGetAttribLocation(mQuadProgram, "a_TexCoord"); + + ShaderUtil.checkGLError(TAG, "Program parameters"); + } + + /** + * Draws the AR background image. The image will be drawn such that virtual content rendered + * with the matrices provided by {@link com.google.ar.core.Camera#getViewMatrix(float[], int)} + * and {@link com.google.ar.core.Camera#getProjectionMatrix(float[], int, float, float)} will + * accurately follow static physical objects. + * This must be called before drawing virtual content. + * + * @param frame The last {@code Frame} returned by {@link Session#update()}. + */ + public void draw(Frame frame) { + // If display rotation changed (also includes view size change), we need to re-query the uv + // coordinates for the screen rect, as they may have changed as well. + if (frame.hasDisplayGeometryChanged()) { + frame.transformDisplayUvCoords(mQuadTexCoord, mQuadTexCoordTransformed); + } + + // No need to test or write depth, the screen quad has arbitrary depth, and is expected + // to be drawn first. + GLES20.glDisable(GLES20.GL_DEPTH_TEST); + GLES20.glDepthMask(false); + + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureId); + + GLES20.glUseProgram(mQuadProgram); + + // Set the vertex positions. + GLES20.glVertexAttribPointer( + mQuadPositionParam, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, mQuadVertices); + + // Set the texture coordinates. + GLES20.glVertexAttribPointer(mQuadTexCoordParam, TEXCOORDS_PER_VERTEX, + GLES20.GL_FLOAT, false, 0, mQuadTexCoordTransformed); + + // Enable vertex arrays + GLES20.glEnableVertexAttribArray(mQuadPositionParam); + GLES20.glEnableVertexAttribArray(mQuadTexCoordParam); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + + // Disable vertex arrays + GLES20.glDisableVertexAttribArray(mQuadPositionParam); + GLES20.glDisableVertexAttribArray(mQuadTexCoordParam); + + // Restore the depth state for further drawing. + GLES20.glDepthMask(true); + GLES20.glEnable(GLES20.GL_DEPTH_TEST); + + ShaderUtil.checkGLError(TAG, "Draw"); + } + + private static final float[] QUAD_COORDS = new float[]{ + -1.0f, -1.0f, 0.0f, + -1.0f, +1.0f, 0.0f, + +1.0f, -1.0f, 0.0f, + +1.0f, +1.0f, 0.0f, + }; + + private static final float[] QUAD_TEXCOORDS = new float[]{ + 0.0f, 1.0f, + 0.0f, 0.0f, + 1.0f, 1.0f, + 1.0f, 0.0f, + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/DisplayRotationHelper.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/DisplayRotationHelper.java new file mode 100644 index 000000000..de892814e --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/DisplayRotationHelper.java @@ -0,0 +1,100 @@ +package io.agora.api.example.examples.advanced.customvideo; + +import android.app.Activity; +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayManager.DisplayListener; +import android.os.Build; +import android.view.Display; +import android.view.WindowManager; + +import androidx.annotation.RequiresApi; + +import com.google.ar.core.Session; + +/** + * Helper to track the display rotations. In particular, the 180 degree rotations are not notified + * by the onSurfaceChanged() callback, and thus they require listening to the android display + * events. + */ +public class DisplayRotationHelper implements DisplayListener { + private boolean mViewportChanged; + private int mViewportWidth; + private int mViewportHeight; + private final Context mContext; + private final Display mDisplay; + + /** + * Constructs the DisplayRotationHelper but does not register the listener yet. + * + * @param context the Android {@link Context}. + */ + @RequiresApi(api = Build.VERSION_CODES.M) + public DisplayRotationHelper(Context context) { + mContext = context; + mDisplay = context.getSystemService(WindowManager.class).getDefaultDisplay(); + } + + /** Registers the display listener. Should be called from . */ + @RequiresApi(api = Build.VERSION_CODES.M) + public void onResume() { + mContext.getSystemService(DisplayManager.class).registerDisplayListener(this, null); + } + + /** Unregisters the display listener. Should be called from . */ + @RequiresApi(api = Build.VERSION_CODES.M) + public void onPause() { + mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this); + } + + /** + * Records a change in surface dimensions. This will be later used by + * {@link #updateSessionIfNeeded(Session)}. Should be called from + * {@link android.opengl.GLSurfaceView.Renderer + * #onSurfaceChanged(javax.microedition.khronos.opengles.GL10, int, int)}. + * + * @param width the updated width of the surface. + * @param height the updated height of the surface. + */ + public void onSurfaceChanged(int width, int height) { + mViewportWidth = width; + mViewportHeight = height; + mViewportChanged = true; + } + + /** + * Updates the session display geometry if a change was posted either by + * {@link #onSurfaceChanged(int, int)} call or by {@link #onDisplayChanged(int)} system + * callback. This function should be called explicitly before each call to + * {@link Session#update()}. This function will also clear the 'pending update' + * (viewportChanged) flag. + * + * @param session the {@link Session} object to update if display geometry changed. + */ + public void updateSessionIfNeeded(Session session) { + if (mViewportChanged) { + int displayRotation = mDisplay.getRotation(); + session.setDisplayGeometry(displayRotation, mViewportWidth, mViewportHeight); + mViewportChanged = false; + } + } + + /** + * Returns the current rotation state of android display. + * Same as {@link Display#getRotation()}. + */ + public int getRotation() { + return mDisplay.getRotation(); + } + + @Override + public void onDisplayAdded(int displayId) {} + + @Override + public void onDisplayRemoved(int displayId) {} + + @Override + public void onDisplayChanged(int displayId) { + mViewportChanged = true; + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/ObjectRenderer.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/ObjectRenderer.java new file mode 100644 index 000000000..fd3d0c735 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/ObjectRenderer.java @@ -0,0 +1,356 @@ +package io.agora.api.example.examples.advanced.customvideo; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.opengl.GLES20; +import android.opengl.GLUtils; +import android.opengl.Matrix; + + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.nio.ShortBuffer; + +import de.javagl.obj.Obj; +import de.javagl.obj.ObjData; +import de.javagl.obj.ObjReader; +import de.javagl.obj.ObjUtils; +import io.agora.api.example.R; + +/** + * Renders an object loaded from an OBJ file in OpenGL. + */ +public class ObjectRenderer { + private static final String TAG = ObjectRenderer.class.getSimpleName(); + + /** + * Blend mode. + * + * @see #setBlendMode(BlendMode) + */ + public enum BlendMode { + /** Multiplies the destination color by the source alpha. */ + Shadow, + /** Normal alpha blending. */ + Grid + } + + private static final int COORDS_PER_VERTEX = 3; + + // Note: the last component must be zero to avoid applying the translational part of the matrix. + private static final float[] LIGHT_DIRECTION = new float[] { 0.250f, 0.866f, 0.433f, 0.0f }; + private float[] mViewLightDirection = new float[4]; + + // Object vertex buffer variables. + private int mVertexBufferId; + private int mVerticesBaseAddress; + private int mTexCoordsBaseAddress; + private int mNormalsBaseAddress; + private int mIndexBufferId; + private int mIndexCount; + + private int mProgram; + private int[] mTextures = new int[1]; + + // Shader location: model view projection matrix. + private int mModelViewUniform; + private int mModelViewProjectionUniform; + + // Shader location: object attributes. + private int mPositionAttribute; + private int mNormalAttribute; + private int mTexCoordAttribute; + + // Shader location: texture sampler. + private int mTextureUniform; + + // Shader location: environment properties. + private int mLightingParametersUniform; + + // Shader location: material properties. + private int mMaterialParametersUniform; + + private BlendMode mBlendMode = null; + + // Temporary matrices allocated here to reduce number of allocations for each frame. + private float[] mModelMatrix = new float[16]; + private float[] mModelViewMatrix = new float[16]; + private float[] mModelViewProjectionMatrix = new float[16]; + + // Set some default material properties to use for lighting. + private float mAmbient = 0.3f; + private float mDiffuse = 1.0f; + private float mSpecular = 1.0f; + private float mSpecularPower = 6.0f; + + public ObjectRenderer() { + } + + /** + * Creates and initializes OpenGL resources needed for rendering the model. + * + * @param context Context for loading the shader and below-named model and texture assets. + * @param objAssetName Name of the OBJ file containing the model geometry. + * @param diffuseTextureAssetName Name of the PNG file containing the diffuse texture map. + */ + public void createOnGlThread(Context context, String objAssetName, + String diffuseTextureAssetName) throws IOException { + // Read the texture. + Bitmap textureBitmap = BitmapFactory.decodeStream( + context.getAssets().open(diffuseTextureAssetName)); + + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glGenTextures(mTextures.length, mTextures, 0); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextures[0]); + + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, textureBitmap, 0); + GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); + + textureBitmap.recycle(); + + ShaderUtil.checkGLError(TAG, "Texture loading"); + + // Read the obj file. + InputStream objInputStream = context.getAssets().open(objAssetName); + Obj obj = ObjReader.read(objInputStream); + + // Prepare the Obj so that its structure is suitable for + // rendering with OpenGL: + // 1. Triangulate it + // 2. Make sure that texture coordinates are not ambiguous + // 3. Make sure that normals are not ambiguous + // 4. Convert it to single-indexed data + obj = ObjUtils.convertToRenderable(obj); + + // OpenGL does not use Java arrays. ByteBuffers are used instead to provide data in a format + // that OpenGL understands. + + // Obtain the data from the OBJ, as direct buffers: + IntBuffer wideIndices = ObjData.getFaceVertexIndices(obj, 3); + FloatBuffer vertices = ObjData.getVertices(obj); + FloatBuffer texCoords = ObjData.getTexCoords(obj, 2); + FloatBuffer normals = ObjData.getNormals(obj); + + // Convert int indices to shorts for GL ES 2.0 compatibility + ShortBuffer indices = ByteBuffer.allocateDirect(2 * wideIndices.limit()) + .order(ByteOrder.nativeOrder()).asShortBuffer(); + while (wideIndices.hasRemaining()) { + indices.put((short) wideIndices.get()); + } + indices.rewind(); + + int[] buffers = new int[2]; + GLES20.glGenBuffers(2, buffers, 0); + mVertexBufferId = buffers[0]; + mIndexBufferId = buffers[1]; + + // Load vertex buffer + mVerticesBaseAddress = 0; + mTexCoordsBaseAddress = mVerticesBaseAddress + 4 * vertices.limit(); + mNormalsBaseAddress = mTexCoordsBaseAddress + 4 * texCoords.limit(); + final int totalBytes = mNormalsBaseAddress + 4 * normals.limit(); + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVertexBufferId); + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, totalBytes, null, GLES20.GL_STATIC_DRAW); + GLES20.glBufferSubData( + GLES20.GL_ARRAY_BUFFER, mVerticesBaseAddress, 4 * vertices.limit(), vertices); + GLES20.glBufferSubData( + GLES20.GL_ARRAY_BUFFER, mTexCoordsBaseAddress, 4 * texCoords.limit(), texCoords); + GLES20.glBufferSubData( + GLES20.GL_ARRAY_BUFFER, mNormalsBaseAddress, 4 * normals.limit(), normals); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + + // Load index buffer + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, mIndexBufferId); + mIndexCount = indices.limit(); + GLES20.glBufferData( + GLES20.GL_ELEMENT_ARRAY_BUFFER, 2 * mIndexCount, indices, GLES20.GL_STATIC_DRAW); + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0); + + ShaderUtil.checkGLError(TAG, "OBJ buffer load"); + + final int vertexShader = ShaderUtil.loadGLShader(TAG, context, + GLES20.GL_VERTEX_SHADER, R.raw.object_vertex); + final int fragmentShader = ShaderUtil.loadGLShader(TAG, context, + GLES20.GL_FRAGMENT_SHADER, R.raw.object_fragment); + + mProgram = GLES20.glCreateProgram(); + GLES20.glAttachShader(mProgram, vertexShader); + GLES20.glAttachShader(mProgram, fragmentShader); + GLES20.glLinkProgram(mProgram); + GLES20.glUseProgram(mProgram); + + ShaderUtil.checkGLError(TAG, "Program creation"); + + mModelViewUniform = GLES20.glGetUniformLocation(mProgram, "u_ModelView"); + mModelViewProjectionUniform = + GLES20.glGetUniformLocation(mProgram, "u_ModelViewProjection"); + + mPositionAttribute = GLES20.glGetAttribLocation(mProgram, "a_Position"); + mNormalAttribute = GLES20.glGetAttribLocation(mProgram, "a_Normal"); + mTexCoordAttribute = GLES20.glGetAttribLocation(mProgram, "a_TexCoord"); + + mTextureUniform = GLES20.glGetUniformLocation(mProgram, "u_Texture"); + + mLightingParametersUniform = GLES20.glGetUniformLocation(mProgram, "u_LightingParameters"); + mMaterialParametersUniform = GLES20.glGetUniformLocation(mProgram, "u_MaterialParameters"); + + ShaderUtil.checkGLError(TAG, "Program parameters"); + + Matrix.setIdentityM(mModelMatrix, 0); + } + + /** + * Selects the blending mode for rendering. + * + * @param blendMode The blending mode. Null indicates no blending (opaque rendering). + */ + public void setBlendMode(BlendMode blendMode) { + mBlendMode = blendMode; + } + + /** + * Updates the object model matrix and applies scaling. + * + * @param modelMatrix A 4x4 model-to-world transformation matrix, stored in column-major order. + * @param scaleFactor A separate scaling factor to apply before the {@code modelMatrix}. + * @see Matrix + */ + public void updateModelMatrix(float[] modelMatrix, float scaleFactor) { + float[] scaleMatrix = new float[16]; + Matrix.setIdentityM(scaleMatrix, 0); + scaleMatrix[0] = scaleFactor; + scaleMatrix[5] = scaleFactor; + scaleMatrix[10] = scaleFactor; + Matrix.multiplyMM(mModelMatrix, 0, modelMatrix, 0, scaleMatrix, 0); + } + + /** + * Sets the surface characteristics of the rendered model. + * + * @param ambient Intensity of non-directional surface illumination. + * @param diffuse Diffuse (matte) surface reflectivity. + * @param specular Specular (shiny) surface reflectivity. + * @param specularPower Surface shininess. Larger values result in a smaller, sharper + * specular highlight. + */ + public void setMaterialProperties( + float ambient, float diffuse, float specular, float specularPower) { + mAmbient = ambient; + mDiffuse = diffuse; + mSpecular = specular; + mSpecularPower = specularPower; + } + + /** + * Draws the model. + * + * @param cameraView A 4x4 view matrix, in column-major order. + * @param cameraPerspective A 4x4 projection matrix, in column-major order. + * @param lightIntensity Illumination intensity. Combined with diffuse and specular material + * properties. + * @see #setBlendMode(BlendMode) + * @see #updateModelMatrix(float[], float) + * @see #setMaterialProperties(float, float, float, float) + * @see Matrix + */ + public void draw(float[] cameraView, float[] cameraPerspective, float lightIntensity) { + + ShaderUtil.checkGLError(TAG, "Before draw"); + + // Build the ModelView and ModelViewProjection matrices + // for calculating object position and light. + Matrix.multiplyMM(mModelViewMatrix, 0, cameraView, 0, mModelMatrix, 0); + Matrix.multiplyMM(mModelViewProjectionMatrix, 0, cameraPerspective, 0, mModelViewMatrix, 0); + + GLES20.glUseProgram(mProgram); + + // Set the lighting environment properties. + Matrix.multiplyMV(mViewLightDirection, 0, mModelViewMatrix, 0, LIGHT_DIRECTION, 0); + normalizeVec3(mViewLightDirection); + GLES20.glUniform4f(mLightingParametersUniform, + mViewLightDirection[0], mViewLightDirection[1], mViewLightDirection[2], lightIntensity); + + // Set the object material properties. + GLES20.glUniform4f(mMaterialParametersUniform, mAmbient, mDiffuse, mSpecular, + mSpecularPower); + + // Attach the object texture. + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextures[0]); + GLES20.glUniform1i(mTextureUniform, 0); + + // Set the vertex attributes. + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVertexBufferId); + + GLES20.glVertexAttribPointer( + mPositionAttribute, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, mVerticesBaseAddress); + GLES20.glVertexAttribPointer( + mNormalAttribute, 3, GLES20.GL_FLOAT, false, 0, mNormalsBaseAddress); + GLES20.glVertexAttribPointer( + mTexCoordAttribute, 2, GLES20.GL_FLOAT, false, 0, mTexCoordsBaseAddress); + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + + // Set the ModelViewProjection matrix in the shader. + GLES20.glUniformMatrix4fv( + mModelViewUniform, 1, false, mModelViewMatrix, 0); + GLES20.glUniformMatrix4fv( + mModelViewProjectionUniform, 1, false, mModelViewProjectionMatrix, 0); + + // Enable vertex arrays + GLES20.glEnableVertexAttribArray(mPositionAttribute); + GLES20.glEnableVertexAttribArray(mNormalAttribute); + GLES20.glEnableVertexAttribArray(mTexCoordAttribute); + + if (mBlendMode != null) { + GLES20.glDepthMask(false); + GLES20.glEnable(GLES20.GL_BLEND); + switch (mBlendMode) { + case Shadow: + // Multiplicative blending function for Shadow. + GLES20.glBlendFunc(GLES20.GL_ZERO, GLES20.GL_ONE_MINUS_SRC_ALPHA); + break; + case Grid: + // Grid, additive blending function. + GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); + break; + } + } + + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, mIndexBufferId); + GLES20.glDrawElements(GLES20.GL_TRIANGLES, mIndexCount, GLES20.GL_UNSIGNED_SHORT, 0); + GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0); + + if (mBlendMode != null) { + GLES20.glDisable(GLES20.GL_BLEND); + GLES20.glDepthMask(true); + } + + // Disable vertex arrays + GLES20.glDisableVertexAttribArray(mPositionAttribute); + GLES20.glDisableVertexAttribArray(mNormalAttribute); + GLES20.glDisableVertexAttribArray(mTexCoordAttribute); + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); + + ShaderUtil.checkGLError(TAG, "After draw"); + } + + private static void normalizeVec3(float[] v) { + float reciprocalLength = 1.0f / (float) Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); + v[0] *= reciprocalLength; + v[1] *= reciprocalLength; + v[2] *= reciprocalLength; + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/PeerRenderer.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/PeerRenderer.java new file mode 100644 index 000000000..5edb2ad0a --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/PeerRenderer.java @@ -0,0 +1,178 @@ +package io.agora.api.example.examples.advanced.customvideo; + +import android.content.Context; +import android.opengl.GLES20; +import android.opengl.Matrix; + +import java.io.IOException; +import java.nio.FloatBuffer; + +import io.agora.api.example.R; +import io.agora.api.example.common.model.Peer; +import io.agora.rtc.gl.GlUtil; + +/** + * Created by wyylling@gmail.com on 03/01/2018. + */ +public class PeerRenderer { + private static final String TAG = PeerRenderer.class.getSimpleName(); + + + private static final int COORDS_PER_VERTEX = 3; + + private int mProgram; + private int[] mTextures = new int[1]; + + // Shader location: object attributes. + private int mPositionAttribute; + private int mTexCoordAttribute; + //private int mTextureLocation; + private int mModelViewProjectionUniform; + + // Temporary matrices allocated here to reduce number of allocations for each frame. + private float[] mModelMatrix = new float[16]; + private float[] mModelViewMatrix = new float[16]; + private float[] mModelViewProjectionMatrix = new float[16]; + + // Vertex coordinates in Normalized Device Coordinates, i.e. (-1, -1) is bottom-left and (1, 1) is + // top-right. + private static final FloatBuffer FULL_RECTANGLE_BUF = GlUtil.createFloatBuffer(new float[] { + -0.16f, -0.16f, // Bottom left. + 0.16f, -0.16f, // Bottom right. + -0.16f, 0.16f, // Top left. + 0.16f, 0.16f, // Top right. + }); + + // Texture coordinates - (0, 0) is bottom-left and (1, 1) is top-right. + private static final FloatBuffer FULL_RECTANGLE_TEX_BUF = GlUtil.createFloatBuffer(new float[] { + 0.0f, 1.0f, // Top left. + 1.0f, 1.0f, // Top right. + 0.0f, 0.0f, // Bottom left. + 1.0f, 0.0f, // Bottom right. + }); + + public PeerRenderer() { + } + + /** + * Creates and initializes OpenGL resources needed for rendering the model. + * + * @param context Context for loading the shader and below-named model and texture assets. + */ + public void createOnGlThread(Context context) throws IOException {; + GLES20.glGenTextures(mTextures.length, mTextures, 0); + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextures[0]); + + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); + + ShaderUtil.checkGLError(TAG, "Texture loading"); + + + final int vertexShader = ShaderUtil.loadGLShader(TAG, context, + GLES20.GL_VERTEX_SHADER, R.raw.peer_vertex); + final int fragmentShader = ShaderUtil.loadGLShader(TAG, context, + GLES20.GL_FRAGMENT_SHADER, R.raw.peer_fragment); + + mProgram = GLES20.glCreateProgram(); + GLES20.glAttachShader(mProgram, vertexShader); + GLES20.glAttachShader(mProgram, fragmentShader); + GLES20.glLinkProgram(mProgram); + GLES20.glUseProgram(mProgram); + + ShaderUtil.checkGLError(TAG, "Program creation"); + + mModelViewProjectionUniform = GLES20.glGetUniformLocation(mProgram, "u_ModelViewProjection"); + //mTextureLocation = GLES20.glGetUniformLocation(mProgram, "rgb_tex"); + //GLES20.glUniform1i(shader.glShader.getUniformLocation("rgb_tex"), 0); + + //mModelViewUniform = GLES20.glGetUniformLocation(mProgram, "u_ModelView"); + //mModelViewProjectionUniform = GLES20.glGetUniformLocation(mProgram, "u_ModelViewProjection"); + + mPositionAttribute = GLES20.glGetAttribLocation(mProgram, "a_Position"); + mTexCoordAttribute = GLES20.glGetAttribLocation(mProgram, "a_TexCoord"); + + ShaderUtil.checkGLError(TAG, "Program parameters"); + + Matrix.setIdentityM(mModelMatrix, 0); + } + + /** + * Updates the object model matrix and applies scaling. + * + * @param modelMatrix A 4x4 model-to-world transformation matrix, stored in column-major order. + * @param scaleFactor A separate scaling factor to apply before the {@code modelMatrix}. + * @see Matrix + */ + public void updateModelMatrix(float[] modelMatrix, float scaleFactor) { + float[] scaleMatrix = new float[16]; + Matrix.setIdentityM(scaleMatrix, 0); + scaleMatrix[0] = scaleFactor; + scaleMatrix[5] = scaleFactor; + scaleMatrix[10] = scaleFactor; + Matrix.multiplyMM(mModelMatrix, 0, modelMatrix, 0, scaleMatrix, 0); + } + + /** + * Draws the model. + * + * @param cameraView A 4x4 view matrix, in column-major order. + * @param cameraPerspective A 4x4 projection matrix, in column-major order. + * @see #updateModelMatrix(float[], float) + * @see Matrix + */ + public void draw(float[] cameraView, float[] cameraPerspective, Peer peer) { + + ShaderUtil.checkGLError(TAG, "Before draw"); + + // Build the ModelView and ModelViewProjection matrices + // for calculating object position and light. + Matrix.multiplyMM(mModelViewMatrix, 0, cameraView, 0, mModelMatrix, 0); + Matrix.multiplyMM(mModelViewProjectionMatrix, 0, cameraPerspective, 0, mModelViewMatrix, 0); + + GLES20.glUseProgram(mProgram); + + GLES20.glUniformMatrix4fv( + mModelViewProjectionUniform, 1, false, mModelViewProjectionMatrix, 0); + //GLES20.glUniform1i(mTextureLocation, 0); + + // Attach the object texture. + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextures[0]); + + GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, peer.width, + peer.height, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, peer.data); + + ShaderUtil.checkGLError(TAG, "upload remote peer data"); + + GLES20.glVertexAttribPointer( + mPositionAttribute, 2, GLES20.GL_FLOAT, false, 0, FULL_RECTANGLE_BUF); + GLES20.glVertexAttribPointer( + mTexCoordAttribute, 2, GLES20.GL_FLOAT, false, 0, FULL_RECTANGLE_TEX_BUF); + + // Enable vertex arrays + GLES20.glEnableVertexAttribArray(mPositionAttribute); + GLES20.glEnableVertexAttribArray(mTexCoordAttribute); + + drawRectangle(0, 0, 512, 512); + + // Disable vertex arrays + GLES20.glDisableVertexAttribArray(mPositionAttribute); + GLES20.glDisableVertexAttribArray(mTexCoordAttribute); + + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); + + ShaderUtil.checkGLError(TAG, "After draw"); + } + + private void drawRectangle(int x, int y, int width, int height) { + // Draw quad. + //GLES20.glViewport(x, y, width, height); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/PlaneRenderer.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/PlaneRenderer.java new file mode 100644 index 000000000..37ccfb615 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/PlaneRenderer.java @@ -0,0 +1,428 @@ +package io.agora.api.example.examples.advanced.customvideo; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import android.opengl.GLUtils; +import android.opengl.Matrix; + +import com.google.ar.core.Camera; +import com.google.ar.core.Plane; +import com.google.ar.core.Pose; +import com.google.ar.core.TrackingState; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.ShortBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +import io.agora.api.example.R; + +/** + * Renders the detected AR planes. + */ +public class PlaneRenderer { + private static final String TAG = PlaneRenderer.class.getSimpleName(); + + private static final int BYTES_PER_FLOAT = Float.SIZE / 8; + private static final int BYTES_PER_SHORT = Short.SIZE / 8; + private static final int COORDS_PER_VERTEX = 3; // x, z, alpha + + private static final int VERTS_PER_BOUNDARY_VERT = 2; + private static final int INDICES_PER_BOUNDARY_VERT = 3; + private static final int INITIAL_BUFFER_BOUNDARY_VERTS = 64; + + private static final int INITIAL_VERTEX_BUFFER_SIZE_BYTES = + BYTES_PER_FLOAT * COORDS_PER_VERTEX * VERTS_PER_BOUNDARY_VERT * INITIAL_BUFFER_BOUNDARY_VERTS; + + private static final int INITIAL_INDEX_BUFFER_SIZE_BYTES = + BYTES_PER_SHORT + * INDICES_PER_BOUNDARY_VERT + * INDICES_PER_BOUNDARY_VERT + * INITIAL_BUFFER_BOUNDARY_VERTS; + + private static final float FADE_RADIUS_M = 0.25f; + private static final float DOTS_PER_METER = 10.0f; + private static final float EQUILATERAL_TRIANGLE_SCALE = (float) (1 / Math.sqrt(3)); + + // Using the "signed distance field" approach to render sharp lines and circles. + // {dotThreshold, lineThreshold, lineFadeSpeed, occlusionScale} + // dotThreshold/lineThreshold: red/green intensity above which dots/lines are present + // lineFadeShrink: lines will fade in between alpha = 1-(1/lineFadeShrink) and 1.0 + // occlusionShrink: occluded planes will fade out between alpha = 0 and 1/occlusionShrink + private static final float[] GRID_CONTROL = {0.2f, 0.4f, 2.0f, 1.5f}; + + private int planeProgram; + private final int[] textures = new int[1]; + + private int planeXZPositionAlphaAttribute; + + private int planeModelUniform; + private int planeModelViewProjectionUniform; + private int textureUniform; + private int lineColorUniform; + private int dotColorUniform; + private int gridControlUniform; + private int planeUvMatrixUniform; + + private FloatBuffer vertexBuffer = + ByteBuffer.allocateDirect(INITIAL_VERTEX_BUFFER_SIZE_BYTES) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + private ShortBuffer indexBuffer = + ByteBuffer.allocateDirect(INITIAL_INDEX_BUFFER_SIZE_BYTES) + .order(ByteOrder.nativeOrder()) + .asShortBuffer(); + + // Temporary lists/matrices allocated here to reduce number of allocations for each frame. + private final float[] modelMatrix = new float[16]; + private final float[] modelViewMatrix = new float[16]; + private final float[] modelViewProjectionMatrix = new float[16]; + private final float[] planeColor = new float[4]; + private final float[] planeAngleUvMatrix = + new float[4]; // 2x2 rotation matrix applied to uv coords. + + private final Map planeIndexMap = new HashMap<>(); + + public PlaneRenderer() { + } + + /** + * Allocates and initializes OpenGL resources needed by the plane renderer. Must be called on the + * OpenGL thread, typically in {@link GLSurfaceView.Renderer#onSurfaceCreated(GL10, EGLConfig)}. + * + * @param context Needed to access shader source and texture PNG. + * @param gridDistanceTextureName Name of the PNG file containing the grid texture. + */ + public void createOnGlThread(Context context, String gridDistanceTextureName) throws IOException { + int vertexShader = + ShaderUtil.loadGLShader(TAG, context, GLES20.GL_VERTEX_SHADER, R.raw.plane_vertex); + int passthroughShader = + ShaderUtil.loadGLShader(TAG, context, GLES20.GL_FRAGMENT_SHADER, R.raw.plane_fragment); + + planeProgram = GLES20.glCreateProgram(); + GLES20.glAttachShader(planeProgram, vertexShader); + GLES20.glAttachShader(planeProgram, passthroughShader); + GLES20.glLinkProgram(planeProgram); + GLES20.glUseProgram(planeProgram); + + ShaderUtil.checkGLError(TAG, "Program creation"); + + // Read the texture. + Bitmap textureBitmap = + BitmapFactory.decodeStream(context.getAssets().open(gridDistanceTextureName)); + + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glGenTextures(textures.length, textures, 0); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]); + + GLES20.glTexParameteri( + GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, textureBitmap, 0); + GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); + + ShaderUtil.checkGLError(TAG, "Texture loading"); + + planeXZPositionAlphaAttribute = GLES20.glGetAttribLocation(planeProgram, "a_XZPositionAlpha"); + + planeModelUniform = GLES20.glGetUniformLocation(planeProgram, "u_Model"); + planeModelViewProjectionUniform = + GLES20.glGetUniformLocation(planeProgram, "u_ModelViewProjection"); + textureUniform = GLES20.glGetUniformLocation(planeProgram, "u_Texture"); + lineColorUniform = GLES20.glGetUniformLocation(planeProgram, "u_lineColor"); + dotColorUniform = GLES20.glGetUniformLocation(planeProgram, "u_dotColor"); + gridControlUniform = GLES20.glGetUniformLocation(planeProgram, "u_gridControl"); + planeUvMatrixUniform = GLES20.glGetUniformLocation(planeProgram, "u_PlaneUvMatrix"); + + ShaderUtil.checkGLError(TAG, "Program parameters"); + } + + /** + * Updates the plane model transform matrix and extents. + */ + private void updatePlaneParameters( + float[] planeMatrix, float extentX, float extentZ, FloatBuffer boundary) { + System.arraycopy(planeMatrix, 0, modelMatrix, 0, 16); + if (boundary == null) { + vertexBuffer.limit(0); + indexBuffer.limit(0); + return; + } + + // Generate a new set of vertices and a corresponding triangle strip index set so that + // the plane boundary polygon has a fading edge. This is done by making a copy of the + // boundary polygon vertices and scaling it down around center to push it inwards. Then + // the index buffer is setup accordingly. + boundary.rewind(); + int boundaryVertices = boundary.limit() / 2; + int numVertices; + int numIndices; + + numVertices = boundaryVertices * VERTS_PER_BOUNDARY_VERT; + // drawn as GL_TRIANGLE_STRIP with 3n-2 triangles (n-2 for fill, 2n for perimeter). + numIndices = boundaryVertices * INDICES_PER_BOUNDARY_VERT; + + if (vertexBuffer.capacity() < numVertices * COORDS_PER_VERTEX) { + int size = vertexBuffer.capacity(); + while (size < numVertices * COORDS_PER_VERTEX) { + size *= 2; + } + vertexBuffer = + ByteBuffer.allocateDirect(BYTES_PER_FLOAT * size) + .order(ByteOrder.nativeOrder()) + .asFloatBuffer(); + } + vertexBuffer.rewind(); + vertexBuffer.limit(numVertices * COORDS_PER_VERTEX); + + if (indexBuffer.capacity() < numIndices) { + int size = indexBuffer.capacity(); + while (size < numIndices) { + size *= 2; + } + indexBuffer = + ByteBuffer.allocateDirect(BYTES_PER_SHORT * size) + .order(ByteOrder.nativeOrder()) + .asShortBuffer(); + } + indexBuffer.rewind(); + indexBuffer.limit(numIndices); + + // Note: when either dimension of the bounding box is smaller than 2*FADE_RADIUS_M we + // generate a bunch of 0-area triangles. These don't get rendered though so it works + // out ok. + float xScale = Math.max((extentX - 2 * FADE_RADIUS_M) / extentX, 0.0f); + float zScale = Math.max((extentZ - 2 * FADE_RADIUS_M) / extentZ, 0.0f); + + while (boundary.hasRemaining()) { + float x = boundary.get(); + float z = boundary.get(); + vertexBuffer.put(x); + vertexBuffer.put(z); + vertexBuffer.put(0.0f); + vertexBuffer.put(x * xScale); + vertexBuffer.put(z * zScale); + vertexBuffer.put(1.0f); + } + + // step 1, perimeter + indexBuffer.put((short) ((boundaryVertices - 1) * 2)); + for (int i = 0; i < boundaryVertices; ++i) { + indexBuffer.put((short) (i * 2)); + indexBuffer.put((short) (i * 2 + 1)); + } + indexBuffer.put((short) 1); + // This leaves us on the interior edge of the perimeter between the inset vertices + // for boundary verts n-1 and 0. + + // step 2, interior: + for (int i = 1; i < boundaryVertices / 2; ++i) { + indexBuffer.put((short) ((boundaryVertices - 1 - i) * 2 + 1)); + indexBuffer.put((short) (i * 2 + 1)); + } + if (boundaryVertices % 2 != 0) { + indexBuffer.put((short) ((boundaryVertices / 2) * 2 + 1)); + } + } + + private void draw(float[] cameraView, float[] cameraPerspective) { + // Build the ModelView and ModelViewProjection matrices + // for calculating cube position and light. + Matrix.multiplyMM(modelViewMatrix, 0, cameraView, 0, modelMatrix, 0); + Matrix.multiplyMM(modelViewProjectionMatrix, 0, cameraPerspective, 0, modelViewMatrix, 0); + + // Set the position of the plane + vertexBuffer.rewind(); + GLES20.glVertexAttribPointer( + planeXZPositionAlphaAttribute, + COORDS_PER_VERTEX, + GLES20.GL_FLOAT, + false, + BYTES_PER_FLOAT * COORDS_PER_VERTEX, + vertexBuffer); + + // Set the Model and ModelViewProjection matrices in the shader. + GLES20.glUniformMatrix4fv(planeModelUniform, 1, false, modelMatrix, 0); + GLES20.glUniformMatrix4fv( + planeModelViewProjectionUniform, 1, false, modelViewProjectionMatrix, 0); + + indexBuffer.rewind(); + GLES20.glDrawElements( + GLES20.GL_TRIANGLE_STRIP, indexBuffer.limit(), GLES20.GL_UNSIGNED_SHORT, indexBuffer); + ShaderUtil.checkGLError(TAG, "Drawing plane"); + } + + static class SortablePlane { + final float distance; + final Plane plane; + + SortablePlane(float distance, Plane plane) { + this.distance = distance; + this.plane = plane; + } + } + + /** + * Draws the collection of tracked planes, with closer planes hiding more distant ones. + * + * @param allPlanes The collection of planes to draw. + * @param cameraPose The pose of the camera, as returned by {@link Camera#getPose()} + * @param cameraPerspective The projection matrix, as returned by {@link + * Camera#getProjectionMatrix(float[], int, float, float)} + */ + public void drawPlanes(Collection allPlanes, Pose cameraPose, float[] cameraPerspective) { + // Planes must be sorted by distance from camera so that we draw closer planes first, and + // they occlude the farther planes. + List sortedPlanes = new ArrayList<>(); + float[] normal = new float[3]; + float cameraX = cameraPose.tx(); + float cameraY = cameraPose.ty(); + float cameraZ = cameraPose.tz(); + for (Plane plane : allPlanes) { + if (plane.getTrackingState() != TrackingState.TRACKING || plane.getSubsumedBy() != null) { + continue; + } + + Pose center = plane.getCenterPose(); + // Get transformed Y axis of plane's coordinate system. + center.getTransformedAxis(1, 1.0f, normal, 0); + // Compute dot product of plane's normal with vector from camera to plane center. + float distance = + (cameraX - center.tx()) * normal[0] + + (cameraY - center.ty()) * normal[1] + + (cameraZ - center.tz()) * normal[2]; + if (distance < 0) { // Plane is back-facing. + continue; + } + sortedPlanes.add(new SortablePlane(distance, plane)); + } + Collections.sort( + sortedPlanes, + new Comparator() { + @Override + public int compare(SortablePlane a, SortablePlane b) { + return Float.compare(a.distance, b.distance); + } + }); + + float[] cameraView = new float[16]; + cameraPose.inverse().toMatrix(cameraView, 0); + + // Planes are drawn with additive blending, masked by the alpha channel for occlusion. + + // Start by clearing the alpha channel of the color buffer to 1.0. + GLES20.glClearColor(1, 1, 1, 1); + GLES20.glColorMask(false, false, false, true); + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + GLES20.glColorMask(true, true, true, true); + + // Disable depth write. + GLES20.glDepthMask(false); + + // Additive blending, masked by alpha channel, clearing alpha channel. + GLES20.glEnable(GLES20.GL_BLEND); + GLES20.glBlendFuncSeparate( + GLES20.GL_DST_ALPHA, GLES20.GL_ONE, // RGB (src, dest) + GLES20.GL_ZERO, GLES20.GL_ONE_MINUS_SRC_ALPHA); // ALPHA (src, dest) + + // Set up the shader. + GLES20.glUseProgram(planeProgram); + + // Attach the texture. + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]); + GLES20.glUniform1i(textureUniform, 0); + + // Shared fragment uniforms. + GLES20.glUniform4fv(gridControlUniform, 1, GRID_CONTROL, 0); + + // Enable vertex arrays + GLES20.glEnableVertexAttribArray(planeXZPositionAlphaAttribute); + + ShaderUtil.checkGLError(TAG, "Setting up to draw planes"); + + for (SortablePlane sortedPlane : sortedPlanes) { + Plane plane = sortedPlane.plane; + float[] planeMatrix = new float[16]; + plane.getCenterPose().toMatrix(planeMatrix, 0); + + updatePlaneParameters( + planeMatrix, plane.getExtentX(), plane.getExtentZ(), plane.getPolygon()); + + // Get plane index. Keep a map to assign same indices to same planes. + Integer planeIndex = planeIndexMap.get(plane); + if (planeIndex == null) { + planeIndex = planeIndexMap.size(); + planeIndexMap.put(plane, planeIndex); + } + + // Set plane color. Computed deterministically from the Plane index. + int colorIndex = planeIndex % PLANE_COLORS_RGBA.length; + colorRgbaToFloat(planeColor, PLANE_COLORS_RGBA[colorIndex]); + GLES20.glUniform4fv(lineColorUniform, 1, planeColor, 0); + GLES20.glUniform4fv(dotColorUniform, 1, planeColor, 0); + + // Each plane will have its own angle offset from others, to make them easier to + // distinguish. Compute a 2x2 rotation matrix from the angle. + float angleRadians = planeIndex * 0.144f; + float uScale = DOTS_PER_METER; + float vScale = DOTS_PER_METER * EQUILATERAL_TRIANGLE_SCALE; + planeAngleUvMatrix[0] = +(float) Math.cos(angleRadians) * uScale; + planeAngleUvMatrix[1] = -(float) Math.sin(angleRadians) * vScale; + planeAngleUvMatrix[2] = +(float) Math.sin(angleRadians) * uScale; + planeAngleUvMatrix[3] = +(float) Math.cos(angleRadians) * vScale; + GLES20.glUniformMatrix2fv(planeUvMatrixUniform, 1, false, planeAngleUvMatrix, 0); + + draw(cameraView, cameraPerspective); + } + + // Clean up the state we set + GLES20.glDisableVertexAttribArray(planeXZPositionAlphaAttribute); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); + GLES20.glDisable(GLES20.GL_BLEND); + GLES20.glDepthMask(true); + + ShaderUtil.checkGLError(TAG, "Cleaning up after drawing planes"); + } + + private static void colorRgbaToFloat(float[] planeColor, int colorRgba) { + planeColor[0] = ((float) ((colorRgba >> 24) & 0xff)) / 255.0f; + planeColor[1] = ((float) ((colorRgba >> 16) & 0xff)) / 255.0f; + planeColor[2] = ((float) ((colorRgba >> 8) & 0xff)) / 255.0f; + planeColor[3] = ((float) ((colorRgba >> 0) & 0xff)) / 255.0f; + } + + private static final int[] PLANE_COLORS_RGBA = { + 0xFFFFFFFF, + 0xF44336FF, + 0xE91E63FF, + 0x9C27B0FF, + 0x673AB7FF, + 0x3F51B5FF, + 0x2196F3FF, + 0x03A9F4FF, + 0x00BCD4FF, + 0x009688FF, + 0x4CAF50FF, + 0x8BC34AFF, + 0xCDDC39FF, + 0xFFEB3BFF, + 0xFFC107FF, + 0xFF9800FF, + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/PointCloudRenderer.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/PointCloudRenderer.java new file mode 100644 index 000000000..2b9fc2fc2 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/PointCloudRenderer.java @@ -0,0 +1,146 @@ +package io.agora.api.example.examples.advanced.customvideo; + +import android.content.Context; +import android.opengl.GLES20; +import android.opengl.GLSurfaceView; +import android.opengl.Matrix; + +import com.google.ar.core.PointCloud; + +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.opengles.GL10; + +import io.agora.api.example.R; + +/** + * Renders a point cloud. + */ +public class PointCloudRenderer { + private static final String TAG = PointCloud.class.getSimpleName(); + + private static final int BYTES_PER_FLOAT = Float.SIZE / 8; + private static final int FLOATS_PER_POINT = 4; // X,Y,Z,confidence. + private static final int BYTES_PER_POINT = BYTES_PER_FLOAT * FLOATS_PER_POINT; + private static final int INITIAL_BUFFER_POINTS = 1000; + + private int mVbo; + private int mVboSize; + + private int mProgramName; + private int mPositionAttribute; + private int mModelViewProjectionUniform; + private int mColorUniform; + private int mPointSizeUniform; + + private int mNumPoints = 0; + + // Keep track of the last point cloud rendered to avoid updating the VBO if point cloud + // was not changed. + private PointCloud mLastPointCloud = null; + + public PointCloudRenderer() { + } + + /** + * Allocates and initializes OpenGL resources needed by the plane renderer. Must be + * called on the OpenGL thread, typically in + * {@link GLSurfaceView.Renderer#onSurfaceCreated(GL10, EGLConfig)}. + * + * @param context Needed to access shader source. + */ + public void createOnGlThread(Context context) { + ShaderUtil.checkGLError(TAG, "before create"); + + int[] buffers = new int[1]; + GLES20.glGenBuffers(1, buffers, 0); + mVbo = buffers[0]; + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVbo); + + mVboSize = INITIAL_BUFFER_POINTS * BYTES_PER_POINT; + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, mVboSize, null, GLES20.GL_DYNAMIC_DRAW); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + + ShaderUtil.checkGLError(TAG, "buffer alloc"); + + int vertexShader = ShaderUtil.loadGLShader(TAG, context, + GLES20.GL_VERTEX_SHADER, R.raw.point_cloud_vertex); + int passthroughShader = ShaderUtil.loadGLShader(TAG, context, + GLES20.GL_FRAGMENT_SHADER, R.raw.passthrough_fragment); + + mProgramName = GLES20.glCreateProgram(); + GLES20.glAttachShader(mProgramName, vertexShader); + GLES20.glAttachShader(mProgramName, passthroughShader); + GLES20.glLinkProgram(mProgramName); + GLES20.glUseProgram(mProgramName); + + ShaderUtil.checkGLError(TAG, "program"); + + mPositionAttribute = GLES20.glGetAttribLocation(mProgramName, "a_Position"); + mColorUniform = GLES20.glGetUniformLocation(mProgramName, "u_Color"); + mModelViewProjectionUniform = GLES20.glGetUniformLocation( + mProgramName, "u_ModelViewProjection"); + mPointSizeUniform = GLES20.glGetUniformLocation(mProgramName, "u_PointSize"); + + ShaderUtil.checkGLError(TAG, "program params"); + } + + /** + * Updates the OpenGL buffer contents to the provided point. Repeated calls with the same + * point cloud will be ignored. + */ + public void update(PointCloud cloud) { + if (mLastPointCloud == cloud) { + // Redundant call. + return; + } + + ShaderUtil.checkGLError(TAG, "before update"); + + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVbo); + mLastPointCloud = cloud; + + // If the VBO is not large enough to fit the new point cloud, resize it. + mNumPoints = mLastPointCloud.getPoints().remaining() / FLOATS_PER_POINT; + if (mNumPoints * BYTES_PER_POINT > mVboSize) { + while (mNumPoints * BYTES_PER_POINT > mVboSize) { + mVboSize *= 2; + } + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, mVboSize, null, GLES20.GL_DYNAMIC_DRAW); + } + GLES20.glBufferSubData(GLES20.GL_ARRAY_BUFFER, 0, mNumPoints * BYTES_PER_POINT, + mLastPointCloud.getPoints()); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + + ShaderUtil.checkGLError(TAG, "after update"); + } + + /** + * Renders the point cloud. ArCore point cloud is given in world space. + * + * @param cameraView the camera view matrix for this frame, typically from {@link + * com.google.ar.core.Camera#getViewMatrix(float[], int)}. + * @param cameraPerspective the camera projection matrix for this frame, typically from {@link + * com.google.ar.core.Camera#getProjectionMatrix(float[], int, float, float)}. + */ + public void draw(float[] cameraView, float[] cameraPerspective) { + float[] modelViewProjection = new float[16]; + Matrix.multiplyMM(modelViewProjection, 0, cameraPerspective, 0, cameraView, 0); + + ShaderUtil.checkGLError(TAG, "Before draw"); + + GLES20.glUseProgram(mProgramName); + GLES20.glEnableVertexAttribArray(mPositionAttribute); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVbo); + GLES20.glVertexAttribPointer( + mPositionAttribute, 4, GLES20.GL_FLOAT, false, BYTES_PER_POINT, 0); + GLES20.glUniform4f(mColorUniform, 31.0f / 255.0f, 188.0f / 255.0f, 210.0f / 255.0f, 1.0f); + GLES20.glUniformMatrix4fv(mModelViewProjectionUniform, 1, false, modelViewProjection, 0); + GLES20.glUniform1f(mPointSizeUniform, 5.0f); + + GLES20.glDrawArrays(GLES20.GL_POINTS, 0, mNumPoints); + GLES20.glDisableVertexAttribArray(mPositionAttribute); + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + + ShaderUtil.checkGLError(TAG, "Draw"); + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/ShaderUtil.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/ShaderUtil.java new file mode 100644 index 000000000..89702ea20 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/advanced/customvideo/ShaderUtil.java @@ -0,0 +1,88 @@ +package io.agora.api.example.examples.advanced.customvideo; + +import android.content.Context; +import android.opengl.GLES20; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +/** + * Shader helper functions. + */ +public class ShaderUtil { + /** + * Converts a raw text file, saved as a resource, into an OpenGL ES shader. + * + * @param type The type of shader we will be creating. + * @param resId The resource ID of the raw text file about to be turned into a shader. + * @return The shader object handler. + */ + public static int loadGLShader(String tag, Context context, int type, int resId) { + String code = readRawTextFile(context, resId); + int shader = GLES20.glCreateShader(type); + GLES20.glShaderSource(shader, code); + GLES20.glCompileShader(shader); + + // Get the compilation status. + final int[] compileStatus = new int[1]; + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0); + + // If the compilation failed, delete the shader. + if (compileStatus[0] == 0) { + Log.e(tag, "Error compiling shader: " + GLES20.glGetShaderInfoLog(shader)); + GLES20.glDeleteShader(shader); + shader = 0; + } + + if (shader == 0) { + throw new RuntimeException("Error creating shader."); + } + + return shader; + } + + /** + * Checks if we've had an error inside of OpenGL ES, and if so what that error is. + * + * @param label Label to report in case of error. + * @throws RuntimeException If an OpenGL error is detected. + */ + public static void checkGLError(String tag, String label) { + int lastError = GLES20.GL_NO_ERROR; + // Drain the queue of all errors. + int error; + while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { + Log.e(tag, label + ": glError " + error); + lastError = error; + } + if (lastError != GLES20.GL_NO_ERROR) { + throw new RuntimeException(label + ": glError " + lastError); + } + } + + /** + * Converts a raw text file into a string. + * + * @param resId The resource ID of the raw text file about to be turned into a shader. + * @return The context of the text file, or null in case of error. + */ + private static String readRawTextFile(Context context, int resId) { + InputStream inputStream = context.getResources().openRawResource(resId); + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + reader.close(); + return sb.toString(); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudio.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudio.java new file mode 100755 index 000000000..3d13398af --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelAudio.java @@ -0,0 +1,350 @@ +package io.agora.api.example.examples.basic; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.models.ChannelMediaOptions; + +import static io.agora.api.example.common.model.Examples.BASIC; + +/** + * This demo demonstrates how to make a one-to-one voice call + * + * @author cjw + */ +@Example( + index = 1, + group = BASIC, + name = R.string.item_joinaudio, + actionId = R.id.action_mainFragment_to_joinChannelAudio, + tipsId = R.string.joinchannelaudio +) +public class JoinChannelAudio extends BaseFragment implements View.OnClickListener { + private static final String TAG = JoinChannelAudio.class.getSimpleName(); + private EditText et_channel; + private Button mute, join, speaker; + private RtcEngine engine; + private int myUid; + private boolean joined = false; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + handler = new Handler(); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_joinchannel_audio, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + et_channel = view.findViewById(R.id.et_channel); + view.findViewById(R.id.btn_join).setOnClickListener(this); + mute = view.findViewById(R.id.btn_mute); + mute.setOnClickListener(this); + speaker = view.findViewById(R.id.btn_speaker); + speaker.setOnClickListener(this); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) { + return; + } + try { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + String appId = getString(R.string.agora_app_id); + engine = RtcEngine.create(getContext().getApplicationContext(), appId, iRtcEngineEventHandler); + } + catch (Exception e) { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if (engine != null) { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + } + + @Override + public void onClick(View v) { + if (v.getId() == R.id.btn_join) { + if (!joined) { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } else { + joined = false; + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + join.setText(getString(R.string.join)); + speaker.setText(getString(R.string.speaker)); + speaker.setEnabled(false); + mute.setText(getString(R.string.closemicrophone)); + mute.setEnabled(false); + } + } else if (v.getId() == R.id.btn_mute) { + mute.setActivated(!mute.isActivated()); + mute.setText(getString(mute.isActivated() ? R.string.openmicrophone : R.string.closemicrophone)); + /**Turn off / on the microphone, stop / start local audio collection and push streaming.*/ + engine.muteLocalAudioStream(mute.isActivated()); + } else if (v.getId() == R.id.btn_speaker) { + speaker.setActivated(!speaker.isActivated()); + speaker.setText(getString(speaker.isActivated() ? R.string.earpiece : R.string.speaker)); + /**Turn off / on the speaker and change the audio playback route.*/ + engine.setEnableSpeakerphone(speaker.isActivated()); + } + } + + /** + * @param channelId Specify the channel name that you want to join. + * Users that input the same channel name join the same channel. + */ + private void joinChannel(String channelId) { + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + engine.enableAudioVolumeIndication(1000, 3, true); + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + Log.e(TAG, RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + + + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() { + @Override + public void run() { + speaker.setEnabled(true); + mute.setEnabled(true); + join.setEnabled(true); + join.setText(getString(R.string.leave)); + } + }); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + } + + @Override + public void onActiveSpeaker(int uid) { + super.onActiveSpeaker(uid); + Log.i(TAG, String.format("onActiveSpeaker:%d", uid)); + } + }; +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelVideo.java b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelVideo.java new file mode 100644 index 000000000..f85a0cae2 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/examples/basic/JoinChannelVideo.java @@ -0,0 +1,533 @@ +package io.agora.api.example.examples.basic; + +import android.content.Context; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatTextView; + +import com.yanzhenjie.permission.AndPermission; +import com.yanzhenjie.permission.runtime.Permission; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import io.agora.api.example.MainApplication; +import io.agora.api.example.R; +import io.agora.api.example.annotation.Example; +import io.agora.api.example.common.BaseFragment; +import io.agora.api.example.common.model.StatisticsInfo; +import io.agora.api.example.utils.CommonUtil; +import io.agora.rtc.Constants; +import io.agora.rtc.IRtcEngineEventHandler; +import io.agora.rtc.RtcEngine; +import io.agora.rtc.models.ChannelMediaOptions; +import io.agora.rtc.video.VideoCanvas; +import io.agora.rtc.video.VideoEncoderConfiguration; + +import static io.agora.api.example.common.model.Examples.BASIC; +import static io.agora.rtc.video.VideoCanvas.RENDER_MODE_HIDDEN; +import static io.agora.rtc.video.VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15; +import static io.agora.rtc.video.VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_ADAPTIVE; +import static io.agora.rtc.video.VideoEncoderConfiguration.STANDARD_BITRATE; +import static io.agora.rtc.video.VideoEncoderConfiguration.VD_640x360; + +/**This demo demonstrates how to make a one-to-one video call*/ +@Example( + index = 0, + group = BASIC, + name = R.string.item_joinvideo, + actionId = R.id.action_mainFragment_to_joinChannelVideo, + tipsId = R.string.joinchannelvideo +) +public class JoinChannelVideo extends BaseFragment implements View.OnClickListener +{ + private static final String TAG = JoinChannelVideo.class.getSimpleName(); + + private FrameLayout fl_local, fl_remote, fl_remote_2, fl_remote_3, fl_remote_4, fl_remote_5; + private Button join; + private EditText et_channel; + private io.agora.rtc.RtcEngine engine; + private int myUid; + private boolean joined = false; + private Map remoteViews = new ConcurrentHashMap(); + private AppCompatTextView localStats, remoteStats; + private StatisticsInfo statisticsInfo; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) + { + View view = inflater.inflate(R.layout.fragment_joinchannel_video, container, false); + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) + { + super.onViewCreated(view, savedInstanceState); + join = view.findViewById(R.id.btn_join); + et_channel = view.findViewById(R.id.et_channel); + view.findViewById(R.id.btn_join).setOnClickListener(this); + fl_local = view.findViewById(R.id.fl_local_video); + fl_remote = view.findViewById(R.id.fl_remote_video); + fl_remote_2 = view.findViewById(R.id.fl_remote2); + fl_remote_3 = view.findViewById(R.id.fl_remote3); + fl_remote_4 = view.findViewById(R.id.fl_remote4); + fl_remote_5 = view.findViewById(R.id.fl_remote5); + localStats = view.findViewById(R.id.local_stats); + localStats.bringToFront(); + remoteStats = view.findViewById(R.id.remote_stats); + remoteStats.bringToFront(); + statisticsInfo = new StatisticsInfo(); + } + + private void updateLocalStats(){ + localStats.setText(statisticsInfo.getLocalVideoStats()); + } + + private void updateRemoteStats(){ + remoteStats.setText(statisticsInfo.getRemoteVideoStats()); + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) + { + super.onActivityCreated(savedInstanceState); + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + try + { + /**Creates an RtcEngine instance. + * @param context The context of Android Activity + * @param appId The App ID issued to you by Agora. See + * How to get the App ID + * @param handler IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events.*/ + engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler); + } + catch (Exception e) + { + e.printStackTrace(); + getActivity().onBackPressed(); + } + } + + @Override + public void onDestroy() + { + super.onDestroy(); + /**leaveChannel and Destroy the RtcEngine instance*/ + if(engine != null) + { + engine.leaveChannel(); + } + handler.post(RtcEngine::destroy); + engine = null; + } + + @Override + public void onClick(View v) + { + if (v.getId() == R.id.btn_join) + { + if (!joined) + { + CommonUtil.hideInputBoard(getActivity(), et_channel); + // call when join button hit + String channelId = et_channel.getText().toString(); + // Check permission + if (AndPermission.hasPermissions(this, Permission.Group.STORAGE, Permission.Group.MICROPHONE, Permission.Group.CAMERA)) + { + joinChannel(channelId); + return; + } + // Request permission + AndPermission.with(this).runtime().permission( + Permission.Group.STORAGE, + Permission.Group.MICROPHONE, + Permission.Group.CAMERA + ).onGranted(permissions -> + { + // Permissions Granted + joinChannel(channelId); + }).start(); + } + else + { + joined = false; + /**After joining a channel, the user must call the leaveChannel method to end the + * call before joining another channel. This method returns 0 if the user leaves the + * channel and releases all resources related to the call. This method call is + * asynchronous, and the user has not exited the channel when the method call returns. + * Once the user leaves the channel, the SDK triggers the onLeaveChannel callback. + * A successful leaveChannel method call triggers the following callbacks: + * 1:The local client: onLeaveChannel. + * 2:The remote client: onUserOffline, if the user leaving the channel is in the + * Communication channel, or is a BROADCASTER in the Live Broadcast profile. + * @returns 0: Success. + * < 0: Failure. + * PS: + * 1:If you call the destroy method immediately after calling the leaveChannel + * method, the leaveChannel process interrupts, and the SDK does not trigger + * the onLeaveChannel callback. + * 2:If you call the leaveChannel method during CDN live streaming, the SDK + * triggers the removeInjectStreamUrl method.*/ + engine.leaveChannel(); + join.setText(getString(R.string.join)); + } + } + } + + private void joinChannel(String channelId) + { + // Check if the context is valid + Context context = getContext(); + if (context == null) + { + return; + } + + // Create render view by RtcEngine + SurfaceView surfaceView = RtcEngine.CreateRendererView(context); + if(fl_local.getChildCount() > 0) + { + fl_local.removeAllViews(); + } + // Add to the local container + fl_local.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup local video to render your local camera preview + engine.setupLocalVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, 0)); + // Set audio route to microPhone + engine.setDefaultAudioRoutetoSpeakerphone(false); + + /** Sets the channel profile of the Agora RtcEngine. + CHANNEL_PROFILE_COMMUNICATION(0): (Default) The Communication profile. + Use this profile in one-on-one calls or group calls, where all users can talk freely. + CHANNEL_PROFILE_LIVE_BROADCASTING(1): The Live-Broadcast profile. Users in a live-broadcast + channel have a role as either broadcaster or audience. A broadcaster can both send and receive streams; + an audience can only receive streams.*/ + engine.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING); + /**In the demo, the default is to enter as the anchor.*/ + engine.setClientRole(IRtcEngineEventHandler.ClientRole.CLIENT_ROLE_BROADCASTER); + // Enable video module + engine.enableVideo(); + // Setup video encoding configs + + engine.setVideoEncoderConfiguration(new VideoEncoderConfiguration( + ((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingDimensionObject(), + VideoEncoderConfiguration.FRAME_RATE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingFrameRate()), + STANDARD_BITRATE, + VideoEncoderConfiguration.ORIENTATION_MODE.valueOf(((MainApplication)getActivity().getApplication()).getGlobalSettings().getVideoEncodingOrientation()) + )); + + /**Please configure accessToken in the string_config file. + * A temporary token generated in Console. A temporary token is valid for 24 hours. For details, see + * https://docs.agora.io/en/Agora%20Platform/token?platform=All%20Platforms#get-a-temporary-token + * A token generated at the server. This applies to scenarios with high-security requirements. For details, see + * https://docs.agora.io/en/cloud-recording/token_server_java?platform=Java*/ + String accessToken = getString(R.string.agora_access_token); + if (TextUtils.equals(accessToken, "") || TextUtils.equals(accessToken, "<#YOUR ACCESS TOKEN#>")) + { + accessToken = null; + } + /** Allows a user to join a channel. + if you do not specify the uid, we will generate the uid for you*/ + + ChannelMediaOptions option = new ChannelMediaOptions(); + option.autoSubscribeAudio = true; + option.autoSubscribeVideo = true; + int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0, option); + if (res != 0) + { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html + showAlert(RtcEngine.getErrorDescription(Math.abs(res))); + return; + } + // Prevent repeated entry + join.setEnabled(false); + } + + /** + * IRtcEngineEventHandler is an abstract class providing default implementation. + * The SDK uses this class to report to the app on SDK runtime events. + */ + private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() + { + /**Reports a warning during SDK runtime. + * Warning code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_warn_code.html*/ + @Override + public void onWarning(int warn) + { + Log.w(TAG, String.format("onWarning code %d message %s", warn, RtcEngine.getErrorDescription(warn))); + } + + /**Reports an error during SDK runtime. + * Error code: https://docs.agora.io/en/Voice/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_i_rtc_engine_event_handler_1_1_error_code.html*/ + @Override + public void onError(int err) + { + Log.e(TAG, String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + showAlert(String.format("onError code %d message %s", err, RtcEngine.getErrorDescription(err))); + /** Upload current log file immediately to server. + * only use this when an error occurs + * block before log file upload success or timeout. + * + * @return + * - 0: Success. + * - < 0: Failure. + */ + engine.uploadLogFile(); + } + + /**Occurs when a user leaves the channel. + * @param stats With this callback, the application retrieves the channel information, + * such as the call duration and statistics.*/ + @Override + public void onLeaveChannel(RtcStats stats) + { + super.onLeaveChannel(stats); + Log.i(TAG, String.format("local user %d leaveChannel!", myUid)); + showLongToast(String.format("local user %d leaveChannel!", myUid)); + } + + /**Occurs when the local user joins a specified channel. + * The channel name assignment is based on channelName specified in the joinChannel method. + * If the uid is not specified when joinChannel is called, the server automatically assigns a uid. + * @param channel Channel name + * @param uid User ID + * @param elapsed Time elapsed (ms) from the user calling joinChannel until this callback is triggered*/ + @Override + public void onJoinChannelSuccess(String channel, int uid, int elapsed) + { + Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + showLongToast(String.format("onJoinChannelSuccess channel %s uid %d", channel, uid)); + myUid = uid; + joined = true; + handler.post(new Runnable() + { + @Override + public void run() + { + join.setEnabled(true); + join.setText(getString(R.string.leave)); + } + }); + } + + /**Since v2.9.0. + * This callback indicates the state change of the remote audio stream. + * PS: This callback does not work properly when the number of users (in the Communication profile) or + * broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the user whose audio state changes. + * @param state State of the remote audio + * REMOTE_AUDIO_STATE_STOPPED(0): The remote audio is in the default state, probably due + * to REMOTE_AUDIO_REASON_LOCAL_MUTED(3), REMOTE_AUDIO_REASON_REMOTE_MUTED(5), + * or REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7). + * REMOTE_AUDIO_STATE_STARTING(1): The first remote audio packet is received. + * REMOTE_AUDIO_STATE_DECODING(2): The remote audio stream is decoded and plays normally, + * probably due to REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2), + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4) or REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6). + * REMOTE_AUDIO_STATE_FROZEN(3): The remote audio is frozen, probably due to + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1). + * REMOTE_AUDIO_STATE_FAILED(4): The remote audio fails to start, probably due to + * REMOTE_AUDIO_REASON_INTERNAL(0). + * @param reason The reason of the remote audio state change. + * REMOTE_AUDIO_REASON_INTERNAL(0): Internal reasons. + * REMOTE_AUDIO_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_AUDIO_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_AUDIO_REASON_LOCAL_MUTED(3): The local user stops receiving the remote audio + * stream or disables the audio module. + * REMOTE_AUDIO_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote audio + * stream or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_MUTED(5): The remote user stops sending the audio stream or + * disables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the audio stream + * or enables the audio module. + * REMOTE_AUDIO_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method + * until the SDK triggers this callback.*/ + @Override + public void onRemoteAudioStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteAudioStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteAudioStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Since v2.9.0. + * Occurs when the remote video state changes. + * PS: This callback does not work properly when the number of users (in the Communication + * profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + * @param uid ID of the remote user whose video state changes. + * @param state State of the remote video: + * REMOTE_VIDEO_STATE_STOPPED(0): The remote video is in the default state, probably due + * to REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3), REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5), + * or REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7). + * REMOTE_VIDEO_STATE_STARTING(1): The first remote video packet is received. + * REMOTE_VIDEO_STATE_DECODING(2): The remote video stream is decoded and plays normally, + * probably due to REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY (2), + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4), REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6), + * or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9). + * REMOTE_VIDEO_STATE_FROZEN(3): The remote video is frozen, probably due to + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1) or REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8). + * REMOTE_VIDEO_STATE_FAILED(4): The remote video fails to start, probably due to + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0). + * @param reason The reason of the remote video state change: + * REMOTE_VIDEO_STATE_REASON_INTERNAL(0): Internal reasons. + * REMOTE_VIDEO_STATE_REASON_NETWORK_CONGESTION(1): Network congestion. + * REMOTE_VIDEO_STATE_REASON_NETWORK_RECOVERY(2): Network recovery. + * REMOTE_VIDEO_STATE_REASON_LOCAL_MUTED(3): The local user stops receiving the remote + * video stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_LOCAL_UNMUTED(4): The local user resumes receiving the remote + * video stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_MUTED(5): The remote user stops sending the video + * stream or disables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_UNMUTED(6): The remote user resumes sending the video + * stream or enables the video module. + * REMOTE_VIDEO_STATE_REASON_REMOTE_OFFLINE(7): The remote user leaves the channel. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK(8): The remote media stream falls back to the + * audio-only stream due to poor network conditions. + * REMOTE_VIDEO_STATE_REASON_AUDIO_FALLBACK_RECOVERY(9): The remote media stream switches + * back to the video stream after the network conditions improve. + * @param elapsed Time elapsed (ms) from the local user calling the joinChannel method until + * the SDK triggers this callback.*/ + @Override + public void onRemoteVideoStateChanged(int uid, int state, int reason, int elapsed) + { + super.onRemoteVideoStateChanged(uid, state, reason, elapsed); + Log.i(TAG, "onRemoteVideoStateChanged->" + uid + ", state->" + state + ", reason->" + reason); + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) joins the channel. + * @param uid ID of the user whose audio state changes. + * @param elapsed Time delay (ms) from the local user calling joinChannel/setClientRole + * until this callback is triggered.*/ + @Override + public void onUserJoined(int uid, int elapsed) + { + super.onUserJoined(uid, elapsed); + Log.i(TAG, "onUserJoined->" + uid); + showLongToast(String.format("user %d joined!", uid)); + /**Check if the context is correct*/ + Context context = getContext(); + if (context == null) { + return; + } + if(remoteViews.containsKey(uid)){ + return; + } + else{ + handler.post(() -> + { + /**Display remote video stream*/ + SurfaceView surfaceView = null; + // Create render view by RtcEngine + surfaceView = RtcEngine.CreateRendererView(context); + surfaceView.setZOrderMediaOverlay(true); + ViewGroup view = getAvailableView(); + remoteViews.put(uid, view); + // Add to the remote container + view.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + // Setup remote video to render + engine.setupRemoteVideo(new VideoCanvas(surfaceView, RENDER_MODE_HIDDEN, uid)); + }); + } + } + + /**Occurs when a remote user (Communication)/host (Live Broadcast) leaves the channel. + * @param uid ID of the user whose audio state changes. + * @param reason Reason why the user goes offline: + * USER_OFFLINE_QUIT(0): The user left the current channel. + * USER_OFFLINE_DROPPED(1): The SDK timed out and the user dropped offline because no data + * packet was received within a certain period of time. If a user quits the + * call and the message is not passed to the SDK (due to an unreliable channel), + * the SDK assumes the user dropped offline. + * USER_OFFLINE_BECOME_AUDIENCE(2): (Live broadcast only.) The client role switched from + * the host to the audience.*/ + @Override + public void onUserOffline(int uid, int reason) + { + Log.i(TAG, String.format("user %d offline! reason:%d", uid, reason)); + showLongToast(String.format("user %d offline! reason:%d", uid, reason)); + handler.post(new Runnable() { + @Override + public void run() { + /**Clear render view + Note: The video will stay at its last frame, to completely remove it you will need to + remove the SurfaceView from its parent*/ + engine.setupRemoteVideo(new VideoCanvas(null, RENDER_MODE_HIDDEN, uid)); + remoteViews.get(uid).removeAllViews(); + remoteViews.remove(uid); + } + }); + } + + @Override + public void onRemoteAudioStats(io.agora.rtc.IRtcEngineEventHandler.RemoteAudioStats remoteAudioStats) { + statisticsInfo.setRemoteAudioStats(remoteAudioStats); + updateRemoteStats(); + } + + @Override + public void onLocalAudioStats(io.agora.rtc.IRtcEngineEventHandler.LocalAudioStats localAudioStats) { + statisticsInfo.setLocalAudioStats(localAudioStats); + updateLocalStats(); + } + + @Override + public void onRemoteVideoStats(io.agora.rtc.IRtcEngineEventHandler.RemoteVideoStats remoteVideoStats) { + statisticsInfo.setRemoteVideoStats(remoteVideoStats); + updateRemoteStats(); + } + + @Override + public void onLocalVideoStats(io.agora.rtc.IRtcEngineEventHandler.LocalVideoStats localVideoStats) { + statisticsInfo.setLocalVideoStats(localVideoStats); + updateLocalStats(); + } + + @Override + public void onRtcStats(io.agora.rtc.IRtcEngineEventHandler.RtcStats rtcStats) { + statisticsInfo.setRtcStats(rtcStats); + } + }; + + private ViewGroup getAvailableView() { + if(fl_remote.getChildCount() == 0){ + return fl_remote; + } + else if(fl_remote_2.getChildCount() == 0){ + return fl_remote_2; + } + else if(fl_remote_3.getChildCount() == 0){ + return fl_remote_3; + } + else if(fl_remote_4.getChildCount() == 0){ + return fl_remote_4; + } + else if(fl_remote_5.getChildCount() == 0){ + return fl_remote_5; + } + else{ + return fl_remote; + } + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/utils/ClassUtils.java b/Android/APIExample/app/src/main/java/io/agora/api/example/utils/ClassUtils.java new file mode 100644 index 000000000..0c281272d --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/utils/ClassUtils.java @@ -0,0 +1,285 @@ +package io.agora.api.example.utils; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Build; +import android.util.Log; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import dalvik.system.DexFile; +import io.agora.api.example.BuildConfig; + +public class ClassUtils +{ + private static final String TAG = ClassUtils.class.getSimpleName(); + private static final String EXTRACTED_NAME_EXT = ".classes"; + private static final String EXTRACTED_SUFFIX = ".zip"; + + private static final String SECONDARY_FOLDER_NAME = "code_cache" + File.separator + "secondary-dexes"; + + private static final String PREFS_FILE = "multidex.version"; + private static final String KEY_DEX_NUMBER = "dex.number"; + + private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2; + private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1; + + private static SharedPreferences getMultiDexPreferences(Context context) + { + return context.getSharedPreferences(PREFS_FILE, Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB ? Context.MODE_PRIVATE : Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS); + } + + /** + * By specifying the package name, scan all ClassName contained under the package + * + * @param context + * @param packageName + * @return Collection of all classes + */ + public static Set getFileNameByPackageName(Context context, final String packageName) throws PackageManager.NameNotFoundException, IOException, InterruptedException + { + final Set classNames = new HashSet<>(); + + List paths = getSourcePaths(context); + final CountDownLatch parserCtl = new CountDownLatch(paths.size()); + + for (final String path : paths) + { + DefaultPoolExecutor.getInstance().execute(new Runnable() + { + @Override + public void run() + { + DexFile dexfile = null; + + try + { + if (path.endsWith(EXTRACTED_SUFFIX)) + { + //NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache" + dexfile = DexFile.loadDex(path, path + ".tmp", 0); + } + else + { + dexfile = new DexFile(path); + } + + Enumeration dexEntries = dexfile.entries(); + while (dexEntries.hasMoreElements()) + { + String className = dexEntries.nextElement(); + if (className.startsWith(packageName)) + { + classNames.add(className); + } + } + } + catch (Throwable ignore) + { + Log.e("ARouter", "Scan map file in dex files made error.", ignore); + } + finally + { + if (null != dexfile) + { + try + { + dexfile.close(); + } + catch (Throwable ignore) + { + } + } + + parserCtl.countDown(); + } + } + }); + } + + parserCtl.await(); + + Log.d(TAG, "Filter " + classNames.size() + " classes by packageName <" + packageName + ">"); + return classNames; + } + + /** + * get all the dex path + * + * @param context the application context + * @return all the dex path + * @throws PackageManager.NameNotFoundException + * @throws IOException + */ + public static List getSourcePaths(Context context) throws PackageManager.NameNotFoundException, IOException + { + ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0); + File sourceApk = new File(applicationInfo.sourceDir); + + List sourcePaths = new ArrayList<>(); + sourcePaths.add(applicationInfo.sourceDir); //add the default apk path + + //the prefix of extracted file, ie: test.classes + String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT; + + /** If MultiDex already supported by VM, we will not to load Classesx.zip from + * Secondary Folder, because there is none.*/ + if (!isVMMultidexCapable()) + { + //the total dex numbers + int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1); + File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME); + + for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) + { + //for each dex file, ie: test.classes2.zip, test.classes3.zip... + String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX; + File extractedFile = new File(dexDir, fileName); + if (extractedFile.isFile()) + { + sourcePaths.add(extractedFile.getAbsolutePath()); + //we ignore the verify zip part + } + else + { + throw new IOException("Missing extracted secondary dex file '" + extractedFile.getPath() + "'"); + } + } + } + + if (BuildConfig.DEBUG) + { // Search instant run support only debuggable + sourcePaths.addAll(tryLoadInstantRunDexFile(applicationInfo)); + } + return sourcePaths; + } + + /** + * Get instant run dex path, used to catch the branch usingApkSplits=false. + */ + private static List tryLoadInstantRunDexFile(ApplicationInfo applicationInfo) + { + List instantRunSourcePaths = new ArrayList<>(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && null != applicationInfo.splitSourceDirs) + { + // add the split apk, normally for InstantRun, and newest version. + instantRunSourcePaths.addAll(Arrays.asList(applicationInfo.splitSourceDirs)); + Log.d(TAG, "Found InstantRun support"); + } + else + { + try + { + // This man is reflection from Google instant run sdk, he will tell me where the dex files go. + Class pathsByInstantRun = Class.forName("com.android.tools.fd.runtime.Paths"); + Method getDexFileDirectory = pathsByInstantRun.getMethod("getDexFileDirectory", String.class); + String instantRunDexPath = (String) getDexFileDirectory.invoke(null, applicationInfo.packageName); + + File instantRunFilePath = new File(instantRunDexPath); + if (instantRunFilePath.exists() && instantRunFilePath.isDirectory()) + { + File[] dexFile = instantRunFilePath.listFiles(); + for (File file : dexFile) + { + if (null != file && file.exists() && file.isFile() && file.getName().endsWith(".dex")) + { + instantRunSourcePaths.add(file.getAbsolutePath()); + } + } + Log.d(TAG, "Found InstantRun support"); + } + + } + catch (Exception e) + { + Log.e(TAG, "InstantRun support error, " + e.getMessage()); + } + } + + return instantRunSourcePaths; + } + + /** + * Identifies if the current VM has a native support for multidex, meaning there is no need for + * additional installation by this library. + * + * @return true if the VM handles multidex + */ + private static boolean isVMMultidexCapable() + { + boolean isMultidexCapable = false; + String vmName = null; + + try + { + if (isYunOS()) + { // YunOS need special judgment + vmName = "'YunOS'"; + isMultidexCapable = Integer.valueOf(System.getProperty("ro.build.version.sdk")) >= 21; + } + else + { // Native Android system + vmName = "'Android'"; + String versionString = System.getProperty("java.vm.version"); + if (versionString != null) + { + Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString); + if (matcher.matches()) + { + try + { + int major = Integer.parseInt(matcher.group(1)); + int minor = Integer.parseInt(matcher.group(2)); + isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR) + || ((major == VM_WITH_MULTIDEX_VERSION_MAJOR) + && (minor >= VM_WITH_MULTIDEX_VERSION_MINOR)); + } + catch (NumberFormatException ignore) + { + // let isMultidexCapable be false + } + } + } + } + } + catch (Exception ignore) + { + + } + + Log.i(TAG, "VM with name " + vmName + (isMultidexCapable ? " has multidex support" : " does not have multidex support")); + return isMultidexCapable; + } + + /** + * Determine whether the system is a YunOS system + */ + private static boolean isYunOS() + { + try + { + String version = System.getProperty("ro.yunos.version"); + String vmName = System.getProperty("java.vm.name"); + return (vmName != null && vmName.toLowerCase().contains("lemur")) + || (version != null && version.trim().length() > 0); + } + catch (Exception ignore) + { + return false; + } + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/utils/CommonUtil.java b/Android/APIExample/app/src/main/java/io/agora/api/example/utils/CommonUtil.java new file mode 100644 index 000000000..b78399dc2 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/utils/CommonUtil.java @@ -0,0 +1,18 @@ +package io.agora.api.example.utils; + +import android.app.Activity; +import android.content.Context; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; + +/** + * @author cjw + */ +public class CommonUtil { + + public static void hideInputBoard(Activity activity, EditText editText) + { + InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(editText.getWindowToken(), 0); + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/utils/DefaultPoolExecutor.java b/Android/APIExample/app/src/main/java/io/agora/api/example/utils/DefaultPoolExecutor.java new file mode 100644 index 000000000..324fc3087 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/utils/DefaultPoolExecutor.java @@ -0,0 +1,97 @@ +package io.agora.api.example.utils; + +import android.util.Log; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * Executors + * + * @version 1.0 + * @since 16/4/28 下午4:07 + */ +public class DefaultPoolExecutor extends ThreadPoolExecutor +{ + private static final String TAG = DefaultPoolExecutor.class.getSimpleName(); + // Thread args + private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); + private static final int INIT_THREAD_COUNT = CPU_COUNT + 1; + private static final int MAX_THREAD_COUNT = INIT_THREAD_COUNT; + private static final long SURPLUS_THREAD_LIFE = 30L; + + private static DefaultPoolExecutor instance; + + public static DefaultPoolExecutor getInstance() + { + if (null == instance) + { + synchronized (DefaultPoolExecutor.class) + { + if (null == instance) + { + instance = new DefaultPoolExecutor( + INIT_THREAD_COUNT, + MAX_THREAD_COUNT, + SURPLUS_THREAD_LIFE, + TimeUnit.SECONDS, + new ArrayBlockingQueue(64), + new DefaultThreadFactory()); + } + } + } + return instance; + } + + private DefaultPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory) + { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, new RejectedExecutionHandler() + { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) + { + Log.e(TAG, "Task rejected, too many task!"); + } + }); + } + + /* thread execution complete, handle possible exceptions. + * @param r the runnable that has completed + * @param t the exception that caused termination, or null if + */ + @Override + protected void afterExecute(Runnable r, Throwable t) + { + super.afterExecute(r, t); + if (t == null && r instanceof Future) + { + try + { + ((Future) r).get(); + } + catch (CancellationException ce) + { + t = ce; + } + catch (ExecutionException ee) + { + t = ee.getCause(); + } + catch (InterruptedException ie) + { + Thread.currentThread().interrupt(); // ignore/reset + } + } + if (t != null) + { + Log.w(TAG, "Running task appeared exception! Thread [" + Thread.currentThread().getName() + "], because [" + t.getMessage() + "]\n" + TextUtils.formatStackTrace(t.getStackTrace())); + } + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/utils/DefaultThreadFactory.java b/Android/APIExample/app/src/main/java/io/agora/api/example/utils/DefaultThreadFactory.java new file mode 100644 index 000000000..7a6f99fca --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/utils/DefaultThreadFactory.java @@ -0,0 +1,53 @@ +package io.agora.api.example.utils; + +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * DefaultThreadFactory + * + * @author zhilong Contact me. + * @version 1.0 + * @since 15/12/25 上午10:51 + */ +public class DefaultThreadFactory implements ThreadFactory +{ + private static final String TAG = DefaultThreadFactory.class.getSimpleName(); + + private static final AtomicInteger poolNumber = new AtomicInteger(1); + + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final ThreadGroup group; + private final String namePrefix; + + public DefaultThreadFactory() + { + SecurityManager s = System.getSecurityManager(); + group = (s != null) ? s.getThreadGroup() : + Thread.currentThread().getThreadGroup(); + namePrefix = "ARouter task pool No." + poolNumber.getAndIncrement() + ", thread No."; + } + + public Thread newThread(@NonNull Runnable runnable) + { + String threadName = namePrefix + threadNumber.getAndIncrement(); + Log.i(TAG, "Thread production, name is [" + threadName + "]"); + Thread thread = new Thread(group, runnable, threadName, 0); + if (thread.isDaemon()) + { //Make non-background thread + thread.setDaemon(false); + } + if (thread.getPriority() != Thread.NORM_PRIORITY) + { + thread.setPriority(Thread.NORM_PRIORITY); + } + + // Catching exceptions in multi-threaded processing + thread.setUncaughtExceptionHandler((thread1, ex) -> Log.i(TAG, "Running task appeared exception! Thread [" + thread1.getName() + "], because [" + ex.getMessage() + "]")); + return thread; + } +} \ No newline at end of file diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/utils/ErrorUtil.java b/Android/APIExample/app/src/main/java/io/agora/api/example/utils/ErrorUtil.java new file mode 100644 index 000000000..23e865bd1 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/utils/ErrorUtil.java @@ -0,0 +1,11 @@ +package io.agora.api.example.utils; + +import io.agora.api.component.Constant; +import io.agora.rtc.Constants; +import io.agora.rtc.RtcEngine; + +/** + * @author cjw + */ +public class ErrorUtil { +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/utils/TextUtils.java b/Android/APIExample/app/src/main/java/io/agora/api/example/utils/TextUtils.java new file mode 100644 index 000000000..0f8034b3a --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/utils/TextUtils.java @@ -0,0 +1,15 @@ +package io.agora.api.example.utils; + +public class TextUtils { + /** + * Print thread stack + */ + public static String formatStackTrace(StackTraceElement[] stackTrace) { + StringBuilder sb = new StringBuilder(); + for (StackTraceElement element : stackTrace) { + sb.append(" at ").append(element.toString()); + sb.append("\n"); + } + return sb.toString(); + } +} diff --git a/Android/APIExample/app/src/main/java/io/agora/api/example/utils/YUVUtils.java b/Android/APIExample/app/src/main/java/io/agora/api/example/utils/YUVUtils.java new file mode 100644 index 000000000..41dd89583 --- /dev/null +++ b/Android/APIExample/app/src/main/java/io/agora/api/example/utils/YUVUtils.java @@ -0,0 +1,148 @@ +package io.agora.api.example.utils; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.ImageFormat; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.YuvImage; +import android.renderscript.Allocation; +import android.renderscript.Element; +import android.renderscript.RenderScript; +import android.renderscript.ScriptIntrinsicBlur; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class YUVUtils { + + public static void encodeI420(byte[] i420, int[] argb, int width, int height) { + final int frameSize = width * height; + + int yIndex = 0; // Y start index + int uIndex = frameSize; // U statt index + int vIndex = frameSize * 5 / 4; // V start index: w*h*5/4 + + int a, R, G, B, Y, U, V; + int index = 0; + for (int j = 0; j < height; j++) { + for (int i = 0; i < width; i++) { + a = (argb[index] & 0xff000000) >> 24; // is not used obviously + R = (argb[index] & 0xff0000) >> 16; + G = (argb[index] & 0xff00) >> 8; + B = (argb[index] & 0xff) >> 0; + + // well known RGB to YUV algorithm + Y = ((66 * R + 129 * G + 25 * B + 128) >> 8) + 16; + U = ((-38 * R - 74 * G + 112 * B + 128) >> 8) + 128; + V = ((112 * R - 94 * G - 18 * B + 128) >> 8) + 128; + + // I420(YUV420p) -> YYYYYYYY UU VV + i420[yIndex++] = (byte) ((Y < 0) ? 0 : ((Y > 255) ? 255 : Y)); + if (j % 2 == 0 && i % 2 == 0) { + i420[uIndex++] = (byte) ((U < 0) ? 0 : ((U > 255) ? 255 : U)); + i420[vIndex++] = (byte) ((V < 0) ? 0 : ((V > 255) ? 255 : V)); + } + index++; + } + } + } + + public static void encodeNV21(byte[] yuv420sp, int[] argb, int width, int height) { + final int frameSize = width * height; + + int yIndex = 0; + int uvIndex = frameSize; + + int a, R, G, B, Y, U, V; + int index = 0; + for (int j = 0; j < height; j++) { + for (int i = 0; i < width; i++) { + a = (argb[index] & 0xff000000) >> 24; // a is not used obviously + R = (argb[index] & 0xff0000) >> 16; + G = (argb[index] & 0xff00) >> 8; + B = (argb[index] & 0xff) >> 0; + + // well known RGB to YUV algorithm + Y = ((66 * R + 129 * G + 25 * B + 128) >> 8) + 16; + U = ((-38 * R - 74 * G + 112 * B + 128) >> 8) + 128; + V = ((112 * R - 94 * G - 18 * B + 128) >> 8) + 128; + + // NV21 has a plane of Y and interleaved planes of VU each sampled by a factor of 2 + // meaning for every 4 Y pixels there are 1 V and 1 U. Note the sampling is every other + // pixel AND every other scanline. + yuv420sp[yIndex++] = (byte) ((Y < 0) ? 0 : ((Y > 255) ? 255 : Y)); + if (j % 2 == 0 && index % 2 == 0) { + yuv420sp[uvIndex++] = (byte) ((V < 0) ? 0 : ((V > 255) ? 255 : V)); + yuv420sp[uvIndex++] = (byte) ((U < 0) ? 0 : ((U > 255) ? 255 : U)); + } + index++; + } + } + } + + public static void swapYU12toYUV420SP(byte[] yu12bytes, byte[] i420bytes, int width, int height, int yStride, int uStride, int vStride) { + System.arraycopy(yu12bytes, 0, i420bytes, 0, yStride * height); + int startPos = yStride * height; + int yv_start_pos_u = startPos; + int yv_start_pos_v = startPos + startPos / 4; + for (int i = 0; i < startPos / 4; i++) { + i420bytes[startPos + 2 * i + 0] = yu12bytes[yv_start_pos_v + i]; + i420bytes[startPos + 2 * i + 1] = yu12bytes[yv_start_pos_u + i]; + } + } + + public static Bitmap i420ToBitmap(int width, int height, int rotation, int bufferLength, byte[] buffer, int yStride, int uStride, int vStride) { + byte[] NV21 = new byte[bufferLength]; + swapYU12toYUV420SP(buffer, NV21, width, height, yStride, uStride, vStride); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + int[] strides = {yStride, yStride}; + YuvImage image = new YuvImage(NV21, ImageFormat.NV21, width, height, strides); + + image.compressToJpeg( + new Rect(0, 0, image.getWidth(), image.getHeight()), + 100, baos); + + // rotate picture when saving to file + Matrix matrix = new Matrix(); + matrix.postRotate(rotation); + byte[] bytes = baos.toByteArray(); + try { + baos.close(); + } + catch (IOException e) { + e.printStackTrace(); + } + return BitmapFactory.decodeByteArray(bytes, 0, bytes.length); + } + + public static Bitmap blur(Context context, Bitmap image, float radius) { + RenderScript rs = RenderScript.create(context); + Bitmap outputBitmap = Bitmap.createBitmap(image.getWidth(), image.getHeight(), Bitmap.Config.ARGB_8888); + Allocation in = Allocation.createFromBitmap(rs, image); + Allocation out = Allocation.createFromBitmap(rs, outputBitmap); + ScriptIntrinsicBlur intrinsicBlur = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); + intrinsicBlur.setRadius(radius); + intrinsicBlur.setInput(in); + intrinsicBlur.forEach(out); + + out.copyTo(outputBitmap); + image.recycle(); + rs.destroy(); + + return outputBitmap; + } + + public static byte[] bitmapToI420(int inputWidth, int inputHeight, Bitmap scaled) { + int[] argb = new int[inputWidth * inputHeight]; + scaled.getPixels(argb, 0, inputWidth, 0, 0, inputWidth, inputHeight); + byte[] yuv = new byte[inputWidth * inputHeight * 3 / 2]; + YUVUtils.encodeI420(yuv, argb, inputWidth, inputHeight); + scaled.recycle(); + return yuv; + } + +} diff --git a/Android/APIExample/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/Android/APIExample/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/Android/APIExample/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Android/APIExample/app/src/main/res/drawable/ic_launcher_background.xml b/Android/APIExample/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/Android/APIExample/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/drawable/icon1024.png b/Android/APIExample/app/src/main/res/drawable/icon1024.png new file mode 100644 index 000000000..d8f28d286 Binary files /dev/null and b/Android/APIExample/app/src/main/res/drawable/icon1024.png differ diff --git a/Android/APIExample/app/src/main/res/layout/activity_example_layout.xml b/Android/APIExample/app/src/main/res/layout/activity_example_layout.xml new file mode 100644 index 000000000..bac3666bb --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/activity_example_layout.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/Android/APIExample/app/src/main/res/layout/activity_main.xml b/Android/APIExample/app/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..400fb109a --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/activity_setting_layout.xml b/Android/APIExample/app/src/main/res/layout/activity_setting_layout.xml new file mode 100644 index 000000000..8ac778f5b --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/activity_setting_layout.xml @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/APIExample/app/src/main/res/layout/fragment_adjust_volume.xml b/Android/APIExample/app/src/main/res/layout/fragment_adjust_volume.xml new file mode 100755 index 000000000..dd3238918 --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_adjust_volume.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_arcore.xml b/Android/APIExample/app/src/main/res/layout/fragment_arcore.xml new file mode 100644 index 000000000..a37d79d13 --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_arcore.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_channel_encryption.xml b/Android/APIExample/app/src/main/res/layout/fragment_channel_encryption.xml new file mode 100644 index 000000000..01af00f57 --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_channel_encryption.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_custom_audiorecord.xml b/Android/APIExample/app/src/main/res/layout/fragment_custom_audiorecord.xml new file mode 100644 index 000000000..2b5d101c0 --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_custom_audiorecord.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_custom_remoterender.xml b/Android/APIExample/app/src/main/res/layout/fragment_custom_remoterender.xml new file mode 100644 index 000000000..2d1ac930b --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_custom_remoterender.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_geo_fencing.xml b/Android/APIExample/app/src/main/res/layout/fragment_geo_fencing.xml new file mode 100644 index 000000000..26d264a7d --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_geo_fencing.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/APIExample/app/src/main/res/layout/fragment_host_across_channel.xml b/Android/APIExample/app/src/main/res/layout/fragment_host_across_channel.xml new file mode 100644 index 000000000..a71bcb175 --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_host_across_channel.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_in_call_report.xml b/Android/APIExample/app/src/main/res/layout/fragment_in_call_report.xml new file mode 100644 index 000000000..f2ef0b03f --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_in_call_report.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/APIExample/app/src/main/res/layout/fragment_join_multi_channel.xml b/Android/APIExample/app/src/main/res/layout/fragment_join_multi_channel.xml new file mode 100644 index 000000000..51bc73fdb --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_join_multi_channel.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_joinchannel_audio.xml b/Android/APIExample/app/src/main/res/layout/fragment_joinchannel_audio.xml new file mode 100755 index 000000000..a3821212f --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_joinchannel_audio.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_joinchannel_video.xml b/Android/APIExample/app/src/main/res/layout/fragment_joinchannel_video.xml new file mode 100644 index 000000000..e3403bfff --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_joinchannel_video.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_live_streaming.xml b/Android/APIExample/app/src/main/res/layout/fragment_live_streaming.xml new file mode 100644 index 000000000..f831ff142 --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_live_streaming.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_main.xml b/Android/APIExample/app/src/main/res/layout/fragment_main.xml new file mode 100644 index 000000000..6dc9c4382 --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_main.xml @@ -0,0 +1,14 @@ + + \ No newline at end of file diff --git a/Android/APIExample/app/src/main/res/layout/fragment_media_player_kit.xml b/Android/APIExample/app/src/main/res/layout/fragment_media_player_kit.xml new file mode 100644 index 000000000..cd894b377 --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_media_player_kit.xml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/APIExample/app/src/main/res/layout/fragment_play_audio_files.xml b/Android/APIExample/app/src/main/res/layout/fragment_play_audio_files.xml new file mode 100644 index 000000000..bd2e1b5f1 --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_play_audio_files.xml @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_precall_test.xml b/Android/APIExample/app/src/main/res/layout/fragment_precall_test.xml new file mode 100755 index 000000000..7c688fe61 --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_precall_test.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_process_rawdata.xml b/Android/APIExample/app/src/main/res/layout/fragment_process_rawdata.xml new file mode 100644 index 000000000..fc51abf6b --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_process_rawdata.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_push_externalvideo.xml b/Android/APIExample/app/src/main/res/layout/fragment_push_externalvideo.xml new file mode 100644 index 000000000..d2dcd58a3 --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_push_externalvideo.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_quick_switch_channel.xml b/Android/APIExample/app/src/main/res/layout/fragment_quick_switch_channel.xml new file mode 100644 index 000000000..ac5e60148 --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_quick_switch_channel.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_raw_audio.xml b/Android/APIExample/app/src/main/res/layout/fragment_raw_audio.xml new file mode 100755 index 000000000..fd59dadbf --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_raw_audio.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_ready_layout.xml b/Android/APIExample/app/src/main/res/layout/fragment_ready_layout.xml new file mode 100644 index 000000000..155790ea9 --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_ready_layout.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/Android/APIExample/app/src/main/res/layout/fragment_rtmp_injection.xml b/Android/APIExample/app/src/main/res/layout/fragment_rtmp_injection.xml new file mode 100644 index 000000000..4cb70ddcd --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_rtmp_injection.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_rtmp_streaming.xml b/Android/APIExample/app/src/main/res/layout/fragment_rtmp_streaming.xml new file mode 100644 index 000000000..c9609db7b --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_rtmp_streaming.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_send_datastream.xml b/Android/APIExample/app/src/main/res/layout/fragment_send_datastream.xml new file mode 100644 index 000000000..6887a7b8c --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_send_datastream.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_set_audio_profile.xml b/Android/APIExample/app/src/main/res/layout/fragment_set_audio_profile.xml new file mode 100644 index 000000000..feb42390d --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_set_audio_profile.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/APIExample/app/src/main/res/layout/fragment_set_video_profile.xml b/Android/APIExample/app/src/main/res/layout/fragment_set_video_profile.xml new file mode 100644 index 000000000..c5dc169c0 --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_set_video_profile.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_stream_encrypt.xml b/Android/APIExample/app/src/main/res/layout/fragment_stream_encrypt.xml new file mode 100644 index 000000000..2d1ac930b --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_stream_encrypt.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_super_resolution.xml b/Android/APIExample/app/src/main/res/layout/fragment_super_resolution.xml new file mode 100644 index 000000000..06893e21e --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_super_resolution.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_switch_camera_screenshare.xml b/Android/APIExample/app/src/main/res/layout/fragment_switch_camera_screenshare.xml new file mode 100644 index 000000000..32406e54e --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_switch_camera_screenshare.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_switch_external_video.xml b/Android/APIExample/app/src/main/res/layout/fragment_switch_external_video.xml new file mode 100644 index 000000000..a7961eadf --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_switch_external_video.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_two_process_screen_share.xml b/Android/APIExample/app/src/main/res/layout/fragment_two_process_screen_share.xml new file mode 100644 index 000000000..6e1fc6e78 --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_two_process_screen_share.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_video_metadata.xml b/Android/APIExample/app/src/main/res/layout/fragment_video_metadata.xml new file mode 100644 index 000000000..d3c785e43 --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_video_metadata.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + diff --git a/Android/APIExample/app/src/main/res/layout/fragment_voice_effects.xml b/Android/APIExample/app/src/main/res/layout/fragment_voice_effects.xml new file mode 100644 index 000000000..d7c3e403c --- /dev/null +++ b/Android/APIExample/app/src/main/res/layout/fragment_voice_effects.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + - - - - - - - + + + + + + + + + + + - + + + + + + - - - - + + - + + - - - - - - - - - + + + + + + - - - - + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -150,171 +312,29 @@ - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - + - - - + + + + + + + + diff --git a/iOS/APIExample/Base.lproj/Popover.storyboard b/iOS/APIExample/Base.lproj/Popover.storyboard index 632ce5c0b..6d2920f57 100644 --- a/iOS/APIExample/Base.lproj/Popover.storyboard +++ b/iOS/APIExample/Base.lproj/Popover.storyboard @@ -1,9 +1,9 @@ - + - + @@ -22,7 +22,7 @@ - + diff --git a/iOS/APIExample/Common/ARKit/ARVideoRenderer.swift b/iOS/APIExample/Common/ARKit/ARVideoRenderer.swift new file mode 100755 index 000000000..14a9ec598 --- /dev/null +++ b/iOS/APIExample/Common/ARKit/ARVideoRenderer.swift @@ -0,0 +1,138 @@ +// +// ARVideoRenderer.swift +// Agora-Video-With-ARKit +// +// Created by GongYuhua on 2017/12/27. +// Copyright © 2017年 Agora.io All rights reserved. +// + +import Foundation +import MetalKit +import SceneKit +import AgoraRtcKit + +class ARVideoRenderer : NSObject { + fileprivate var yTexture: MTLTexture? + fileprivate var uTexture: MTLTexture? + fileprivate var vTexture: MTLTexture? + fileprivate var rgbTexture: MTLTexture? + + fileprivate let device = MTLCreateSystemDefaultDevice() + fileprivate var commandQueue: MTLCommandQueue? + + fileprivate var defaultLibrary: MTLLibrary? + + fileprivate var threadsPerThreadgroup = MTLSizeMake(16, 16, 1) + fileprivate var threadgroupsPerGrid = MTLSizeMake(128, 96, 1) + fileprivate var pipelineState: MTLComputePipelineState? + + var renderNode: SCNNode? +} + +extension ARVideoRenderer: AgoraVideoSinkProtocol { + func shouldInitialize() -> Bool { + defaultLibrary = device?.makeDefaultLibrary() + + if let device = device, let function = defaultLibrary?.makeFunction(name: "writeRGBFromYUV") { + pipelineState = try? device.makeComputePipelineState(function: function) + } + + commandQueue = device?.makeCommandQueue() + + return true + } + + func shouldStart() { + + } + + func shouldStop() { + + } + + func shouldDispose() { + yTexture = nil + uTexture = nil + vTexture = nil + rgbTexture = nil + + renderNode?.geometry?.firstMaterial?.diffuse.contents = createEmptyRGBTexture(width: 1, height: 1) + } + + func bufferType() -> AgoraVideoBufferType { + return .rawData + } + + func pixelFormat() -> AgoraVideoPixelFormat { + return .I420 + } + + func renderRawData(_ rawData: UnsafeMutableRawPointer, size: CGSize, rotation: AgoraVideoRotation) { + guard let node = renderNode else { + return + } + + let width = Int(size.width) + let height = Int(size.height) + + yTexture = createTexture(withData: rawData, + width: width, + height: height) + uTexture = createTexture(withData: rawData + width * height, + width: width / 2, + height: height / 2) + vTexture = createTexture(withData: rawData + width * height * 5 / 4, + width: width / 2, + height: height / 2) + + rgbTexture = createEmptyRGBTexture(width: width, height: height) + + node.geometry?.firstMaterial?.diffuse.contents = rgbTexture + renderRGBTexture() + } +} + +private extension ARVideoRenderer { + func createTexture(withData data: UnsafeMutableRawPointer, width: Int, height: Int) -> MTLTexture? { + let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .r8Uint, + width: width, + height: height, + mipmapped: false) + let texture = device?.makeTexture(descriptor: descriptor) + texture?.replace(region: MTLRegionMake2D(0, 0, width, height), + mipmapLevel: 0, + withBytes: data, + bytesPerRow: width) + + return texture + } + + func createEmptyRGBTexture(width: Int, height: Int) -> MTLTexture? { + let rgbaDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .rgba16Float, + width: width, + height: height, + mipmapped: false) + rgbaDescriptor.usage = [.shaderWrite, .shaderRead] + let rgbTexture = device?.makeTexture(descriptor: rgbaDescriptor) + return rgbTexture + } + + func renderRGBTexture() { + guard let state = pipelineState, + let buffer = commandQueue?.makeCommandBuffer(), + let encoder = buffer.makeComputeCommandEncoder() else { + return + } + + encoder.setComputePipelineState(state) + encoder.setTexture(yTexture, index: 0) + encoder.setTexture(uTexture, index: 1) + encoder.setTexture(vTexture, index: 2) + encoder.setTexture(rgbTexture, index: 3) + encoder.dispatchThreadgroups(threadgroupsPerGrid, + threadsPerThreadgroup: threadsPerThreadgroup) + encoder.endEncoding() + + buffer.commit() + } +} diff --git a/iOS/APIExample/Common/ARKit/ARVideoSource.swift b/iOS/APIExample/Common/ARKit/ARVideoSource.swift new file mode 100644 index 000000000..28218d596 --- /dev/null +++ b/iOS/APIExample/Common/ARKit/ARVideoSource.swift @@ -0,0 +1,39 @@ +// +// ARVideoSource.swift +// Agora-Video-With-ARKit +// +// Created by GongYuhua on 2018/1/11. +// Copyright © 2018年 Agora. All rights reserved. +// + +import UIKit +import AgoraRtcKit + +class ARVideoSource: NSObject, AgoraVideoSourceProtocol { + var consumer: AgoraVideoFrameConsumer? + + func shouldInitialize() -> Bool { return true } + + func shouldStart() { } + + func shouldStop() { } + + func shouldDispose() { } + + func bufferType() -> AgoraVideoBufferType { + return .pixelBuffer + } + + func contentHint() -> AgoraVideoContentHint { + return .none + } + + func captureType() -> AgoraVideoCaptureType { + return .camera + } + + func sendBuffer(_ buffer: CVPixelBuffer, timestamp: TimeInterval) { + let time = CMTime(seconds: timestamp, preferredTimescale: 1000) + consumer?.consumePixelBuffer(buffer, withTimestamp: time, rotation: .rotation90) + } +} diff --git a/iOS/APIExample/Common/AgoraExtension.swift b/iOS/APIExample/Common/AgoraExtension.swift index 13d38a541..bacf01891 100644 --- a/iOS/APIExample/Common/AgoraExtension.swift +++ b/iOS/APIExample/Common/AgoraExtension.swift @@ -37,16 +37,290 @@ extension AgoraWarningCode { extension AgoraNetworkQuality { func description() -> String { switch self { - case .excellent: return "excellent" + case .excellent: return "excel" case .good: return "good" case .poor: return "poor" case .bad: return "bad" - case .vBad: return "very bad" + case .vBad: return "vBad" case .down: return "down" - case .unknown: return "unknown" + case .unknown: return "NA" case .unsupported: return "unsupported" case .detecting: return "detecting" - default: return "unknown" + default: return "NA" } } } + +extension AgoraVideoOutputOrientationMode { + func description() -> String { + switch self { + case .fixedPortrait: return "fixed portrait".localized + case .fixedLandscape: return "fixed landscape".localized + case .adaptative: return "adaptive".localized + default: return "\(self.rawValue)" + } + } +} + +extension AgoraClientRole { + func description() -> String { + switch self { + case .broadcaster: return "Broadcaster".localized + case .audience: return "Audience".localized + default: + return "\(self.rawValue)" + } + } +} + +extension AgoraAudioProfile { + func description() -> String { + switch self { + case .default: return "Default".localized + case .musicStandard: return "Music Standard".localized + case .musicStandardStereo: return "Music Standard Stereo".localized + case .musicHighQuality: return "Music High Quality".localized + case .musicHighQualityStereo: return "Music High Quality Stereo".localized + case .speechStandard: return "Speech Standard".localized + default: + return "\(self.rawValue)" + } + } + static func allValues() -> [AgoraAudioProfile] { + return [.default, .speechStandard, .musicStandard, .musicStandardStereo, .musicHighQuality, .musicHighQualityStereo] + } +} + +extension AgoraAudioScenario { + func description() -> String { + switch self { + case .default: return "Default".localized + case .chatRoomGaming: return "Chat Room Gaming".localized + case .education: return "Education".localized + case .gameStreaming: return "Game Streaming".localized + case .chatRoomEntertainment: return "Chat Room Entertainment".localized + case .showRoom: return "Show Room".localized + default: + return "\(self.rawValue)" + } + } + + static func allValues() -> [AgoraAudioScenario] { + return [.default, .chatRoomGaming, .education, .gameStreaming, .chatRoomEntertainment, .showRoom] + } +} + +extension AgoraEncryptionMode { + func description() -> String { + switch self { + case .AES128XTS: return "AES128XTS" + case .AES256XTS: return "AES256XTS" + case .AES128ECB: return "AES128ECB" + case .SM4128ECB: return "SM4128ECB" + default: + return "\(self.rawValue)" + } + } + + static func allValues() -> [AgoraEncryptionMode] { + return [.AES128XTS, .AES256XTS, .AES128ECB, .SM4128ECB] + } +} + +extension AgoraAudioVoiceChanger { + func description() -> String { + switch self { + case .voiceChangerOff:return "Off".localized + case .generalBeautyVoiceFemaleFresh:return "FemaleFresh".localized + case .generalBeautyVoiceFemaleVitality:return "FemaleVitality".localized + case .generalBeautyVoiceMaleMagnetic:return "MaleMagnetic".localized + case .voiceBeautyVigorous:return "Vigorous".localized + case .voiceBeautyDeep:return "Deep".localized + case .voiceBeautyMellow:return "Mellow".localized + case .voiceBeautyFalsetto:return "Falsetto".localized + case .voiceBeautyFull:return "Full".localized + case .voiceBeautyClear:return "Clear".localized + case .voiceBeautyResounding:return "Resounding".localized + case .voiceBeautyRinging:return "Ringing".localized + case .voiceBeautySpacial:return "Spacial".localized + case .voiceChangerEthereal:return "Ethereal".localized + case .voiceChangerOldMan:return "Old Man".localized + case .voiceChangerBabyBoy:return "Baby Boy".localized + case .voiceChangerBabyGirl:return "Baby Girl".localized + case .voiceChangerZhuBaJie:return "ZhuBaJie".localized + case .voiceChangerHulk:return "Hulk".localized + default: + return "\(self.rawValue)" + } + } +} + +extension AgoraVoiceBeautifierPreset{ + func description() -> String { + switch self { + case .voiceBeautifierOff:return "Off".localized + case .chatBeautifierFresh:return "FemaleFresh".localized + case .chatBeautifierMagnetic:return "MaleMagnetic".localized + case .chatBeautifierVitality:return "FemaleVitality".localized + case .timbreTransformationVigorous:return "Vigorous".localized + case .timbreTransformationDeep:return "Deep".localized + case .timbreTransformationMellow:return "Mellow".localized + case .timbreTransformationFalsetto:return "Falsetto".localized + case .timbreTransformationFull:return "Full".localized + case .timbreTransformationClear:return "Clear".localized + case .timbreTransformationResounding:return "Resounding".localized + case .timbreTransformationRinging:return "Ringing".localized + default: + return "\(self.rawValue)" + } + } +} + +extension AgoraAudioReverbPreset { + func description() -> String { + switch self { + case .off:return "Off".localized + case .fxUncle:return "FxUncle".localized + case .fxSister:return "FxSister".localized + case .fxPopular:return "Pop".localized + case .popular:return "Pop(Old Version)".localized + case .fxRNB:return "R&B".localized + case .rnB:return "R&B(Old Version)".localized + case .rock:return "Rock".localized + case .hipHop:return "HipHop".localized + case .fxVocalConcert:return "Vocal Concert".localized + case .vocalConcert:return "Vocal Concert(Old Version)".localized + case .fxKTV:return "KTV".localized + case .KTV:return "KTV(Old Version)".localized + case .fxStudio:return "Studio".localized + case .studio:return "Studio(Old Version)".localized + case .fxPhonograph:return "Phonograph".localized + case .virtualStereo:return "Virtual Stereo".localized + default: + return "\(self.rawValue)" + } + } +} + +extension AgoraAudioEffectPreset { + func description() -> String { + switch self { + case .audioEffectOff:return "Off".localized + case .voiceChangerEffectUncle:return "FxUncle".localized + case .voiceChangerEffectOldMan:return "Old Man".localized + case .voiceChangerEffectBoy:return "Baby Boy".localized + case .voiceChangerEffectSister:return "FxSister".localized + case .voiceChangerEffectGirl:return "Baby Girl".localized + case .voiceChangerEffectPigKing:return "ZhuBaJie".localized + case .voiceChangerEffectHulk:return "Hulk".localized + case .styleTransformationRnB:return "R&B".localized + case .styleTransformationPopular:return "Pop".localized + case .roomAcousticsKTV:return "KTV".localized + case .roomAcousticsVocalConcert:return "Vocal Concert".localized + case .roomAcousticsStudio:return "Studio".localized + case .roomAcousticsPhonograph:return "Phonograph".localized + case .roomAcousticsVirtualStereo:return "Virtual Stereo".localized + case .roomAcousticsSpacial:return "Spacial".localized + case .roomAcousticsEthereal:return "Ethereal".localized + case .roomAcoustics3DVoice:return "3D Voice".localized + case .pitchCorrection:return "Pitch Correction".localized + default: + return "\(self.rawValue)" + } + } +} + +extension AgoraAudioEqualizationBandFrequency { + func description() -> String { + switch self { + case .band31: return "31Hz" + case .band62: return "62Hz" + case .band125: return "125Hz" + case .band250: return "250Hz" + case .band500: return "500Hz" + case .band1K: return "1kHz" + case .band2K: return "2kHz" + case .band4K: return "4kHz" + case .band8K: return "8kHz" + case .band16K: return "16kHz" + @unknown default: + return "\(self.rawValue)" + } + } +} + +extension AgoraAudioReverbType { + func description() -> String { + switch self { + case .dryLevel: return "Dry Level".localized + case .wetLevel: return "Wet Level".localized + case .roomSize: return "Room Size".localized + case .wetDelay: return "Wet Delay".localized + case .strength: return "Strength".localized + @unknown default: + return "\(self.rawValue)" + } + } +} + +extension UIAlertController { + func addCancelAction() { + self.addAction(UIAlertAction(title: "Cancel".localized, style: .cancel, handler: nil)) + } +} + +extension UIApplication { + /// The top most view controller + static var topMostViewController: UIViewController? { + return UIApplication.shared.keyWindow?.rootViewController?.visibleViewController + } +} + +extension UIViewController { + /// The visible view controller from a given view controller + var visibleViewController: UIViewController? { + if let navigationController = self as? UINavigationController { + return navigationController.topViewController?.visibleViewController + } else if let tabBarController = self as? UITabBarController { + return tabBarController.selectedViewController?.visibleViewController + } else if let presentedViewController = presentedViewController { + return presentedViewController.visibleViewController + } else { + return self + } + } +} + +extension OutputStream { + + /// Write `String` to `OutputStream` + /// + /// - parameter string: The `String` to write. + /// - parameter encoding: The `String.Encoding` to use when writing the string. This will default to `.utf8`. + /// - parameter allowLossyConversion: Whether to permit lossy conversion when writing the string. Defaults to `false`. + /// + /// - returns: Return total number of bytes written upon success. Return `-1` upon failure. + + func write(_ string: String, encoding: String.Encoding = .utf8, allowLossyConversion: Bool = false) -> Int { + + if let data = string.data(using: encoding, allowLossyConversion: allowLossyConversion) { + let ret = data.withUnsafeBytes { + write($0, maxLength: data.count) + } + if(ret < 0) { + print("write fail: \(streamError.debugDescription)") + } + } + + return -1 + } + +} + +extension Date { + func getFormattedDate(format: String) -> String { + let dateformat = DateFormatter() + dateformat.dateFormat = format + return dateformat.string(from: self) + } +} diff --git a/iOS/APIExample/Common/BaseViewController.swift b/iOS/APIExample/Common/BaseViewController.swift index 85fb2a70a..b8b12f86a 100644 --- a/iOS/APIExample/Common/BaseViewController.swift +++ b/iOS/APIExample/Common/BaseViewController.swift @@ -6,16 +6,17 @@ // Copyright © 2020 Agora Corp. All rights reserved. // -import Foundation import UIKit import AGEVideoLayout -class BaseViewController: UIViewController { + +class BaseViewController: AGViewController { + var configs: [String:Any] = [:] override func viewDidLoad() { - self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Show Log", - style: .plain, - target: self, - action: #selector(showLog)) +// self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Show Log", +// style: .plain, +// target: self, +// action: #selector(showLog)) LogUtils.removeAll() } @@ -25,24 +26,20 @@ class BaseViewController: UIViewController { self.present(newViewController, animated: true, completion: nil) } - func showAlert(title: String?, msg:String) { - let alertController = UIAlertController(title: title, message: msg, preferredStyle: .alert) + func showAlert(title: String? = nil, message: String) { + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) let action = UIAlertAction(title: "OK", style: .cancel, handler: nil) alertController.addAction(action) self.present(alertController, animated: true, completion: nil) } -} - -class RenderViewController: UIViewController { - private var streamViews: [UIView]? - override func viewDidLoad() { - super.viewDidLoad() + func getAudioLabel(uid:UInt, isLocal:Bool) -> String { + return "AUDIO ONLY\n\(isLocal ? "Local" : "Remote")\n\(uid)" } - - func layoutStream(views: [UIView]) { - self.streamViews = views - let container = self.view as! AGEVideoContainer +} + +extension AGEVideoContainer { + func layoutStream(views: [AGView]) { let count = views.count var layout: AGEVideoLayout @@ -60,18 +57,117 @@ class RenderViewController: UIViewController { return } - container.listCount { [unowned self] (level) -> Int in - return self.streamViews?.count ?? 0 - }.listItem { [unowned self] (index) -> AGEView in - return self.streamViews![index.item] + self.listCount { (level) -> Int in + return views.count + }.listItem { (index) -> AGEView in + return views[index.item] } - container.setLayouts([layout]) + self.setLayouts([layout]) } -} - -class RTCViewController: BaseViewController { - override func viewDidLoad() { - super.viewDidLoad() + + func layoutStream1x2(views: [AGView]) { + let count = views.count + + var layout: AGEVideoLayout + + if count > 2 { + return + } else { + layout = AGEVideoLayout(level: 0) + .itemSize(.scale(CGSize(width: 1, height: 0.5))) + } + + self.listCount { (level) -> Int in + return views.count + }.listItem { (index) -> AGEView in + return views[index.item] + } + + self.setLayouts([layout]) + } + + func layoutStream2x1(views: [AGView]) { + let count = views.count + + var layout: AGEVideoLayout + + if count > 2 { + return + } else { + layout = AGEVideoLayout(level: 0) + .itemSize(.scale(CGSize(width: 0.5, height: 1))) + } + + self.listCount { (level) -> Int in + return views.count + }.listItem { (index) -> AGEView in + return views[index.item] + } + + self.setLayouts([layout]) + } + + func layoutStream2x2(views: [AGView]) { + let count = views.count + + var layout: AGEVideoLayout + + if count > 4 { + return + } else { + layout = AGEVideoLayout(level: 0) + .itemSize(.scale(CGSize(width: 0.5, height: 0.5))) + } + + self.listCount { (level) -> Int in + return views.count + }.listItem { (index) -> AGEView in + return views[index.item] + } + + self.setLayouts([layout]) + } + + func layoutStream3x2(views: [AGView]) { + let count = views.count + + var layout: AGEVideoLayout + + if count > 6 { + return + } else { + layout = AGEVideoLayout(level: 0) + .itemSize(.scale(CGSize(width: 0.33, height: 0.5))) + } + + self.listCount { (level) -> Int in + return views.count + }.listItem { (index) -> AGEView in + return views[index.item] + } + + self.setLayouts([layout]) + } + + func layoutStream3x3(views: [AGView]) { + let count = views.count + + var layout: AGEVideoLayout + + if count > 9 { + return + } else { + layout = AGEVideoLayout(level: 0) + .itemSize(.scale(CGSize(width: 0.33, height: 0.33))) + } + + self.listCount { (level) -> Int in + return views.count + }.listItem { (index) -> AGEView in + return views[index.item] + } + + self.setLayouts([layout]) } } diff --git a/iOS/APIExample/Common/CustomEncryption/AgoraCustomEncryption.h b/iOS/APIExample/Common/CustomEncryption/AgoraCustomEncryption.h new file mode 100644 index 000000000..377019342 --- /dev/null +++ b/iOS/APIExample/Common/CustomEncryption/AgoraCustomEncryption.h @@ -0,0 +1,18 @@ +// +// AgoraCustomEncryption.h +// AgoraRtcCustomizedEncryptionTutorial +// +// Created by suleyu on 2018/7/6. +// Copyright © 2018 Agora.io. All rights reserved. +// + +#import +#import + +@interface AgoraCustomEncryption : NSObject + ++ (void)registerPacketProcessing:(AgoraRtcEngineKit *)rtcEngineKit; + ++ (void)deregisterPacketProcessing:(AgoraRtcEngineKit *)rtcEngineKit; + +@end diff --git a/iOS/APIExample/Common/CustomEncryption/AgoraCustomEncryption.mm b/iOS/APIExample/Common/CustomEncryption/AgoraCustomEncryption.mm new file mode 100644 index 000000000..713c055e6 --- /dev/null +++ b/iOS/APIExample/Common/CustomEncryption/AgoraCustomEncryption.mm @@ -0,0 +1,122 @@ +// +// AgoraCustomEncryption.m +// AgoraRtcCustomizedEncryptionTutorial +// +// Created by suleyu on 2018/7/6. +// Copyright © 2018 Agora.io. All rights reserved. +// + +#import "AgoraCustomEncryption.h" + +#include +#include + +class AgoraCustomEncryptionObserver : public agora::rtc::IPacketObserver +{ +public: + AgoraCustomEncryptionObserver() + { + m_txAudioBuffer.resize(2048); + m_rxAudioBuffer.resize(2048); + m_txVideoBuffer.resize(2048); + m_rxVideoBuffer.resize(2048); + } + virtual bool onSendAudioPacket(Packet& packet) + { + int i; + //encrypt the packet + const unsigned char* p = packet.buffer; + const unsigned char* pe = packet.buffer+packet.size; + + + for (i = 0; p < pe && i < m_txAudioBuffer.size(); ++p, ++i) + { + m_txAudioBuffer[i] = *p ^ 0x55; + } + //assign new buffer and the length back to SDK + packet.buffer = &m_txAudioBuffer[0]; + packet.size = i; + return true; + } + + virtual bool onSendVideoPacket(Packet& packet) + { + int i; + //encrypt the packet + const unsigned char* p = packet.buffer; + const unsigned char* pe = packet.buffer+packet.size; + for (i = 0; p < pe && i < m_txVideoBuffer.size(); ++p, ++i) + { + m_txVideoBuffer[i] = *p ^ 0x55; + } + //assign new buffer and the length back to SDK + packet.buffer = &m_txVideoBuffer[0]; + packet.size = i; + return true; + } + + virtual bool onReceiveAudioPacket(Packet& packet) + { + int i = 0; + //decrypt the packet + const unsigned char* p = packet.buffer; + const unsigned char* pe = packet.buffer+packet.size; + for (i = 0; p < pe && i < m_rxAudioBuffer.size(); ++p, ++i) + { + m_rxAudioBuffer[i] = *p ^ 0x55; + } + //assign new buffer and the length back to SDK + packet.buffer = &m_rxAudioBuffer[0]; + packet.size = i; + return true; + } + + virtual bool onReceiveVideoPacket(Packet& packet) + { + int i = 0; + //decrypt the packet + const unsigned char* p = packet.buffer; + const unsigned char* pe = packet.buffer+packet.size; + + + for (i = 0; p < pe && i < m_rxVideoBuffer.size(); ++p, ++i) + { + m_rxVideoBuffer[i] = *p ^ 0x55; + } + //assign new buffer and the length back to SDK + packet.buffer = &m_rxVideoBuffer[0]; + packet.size = i; + return true; + } + +private: + std::vector m_txAudioBuffer; //buffer for sending audio data + std::vector m_txVideoBuffer; //buffer for sending video data + + std::vector m_rxAudioBuffer; //buffer for receiving audio data + std::vector m_rxVideoBuffer; //buffer for receiving video data +}; + +static AgoraCustomEncryptionObserver s_packetObserver; + +@implementation AgoraCustomEncryption + ++ (void)registerPacketProcessing:(AgoraRtcEngineKit *)rtcEngineKit { + if (!rtcEngineKit) { + return; + } + + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)rtcEngineKit.getNativeHandle; + rtc_engine->registerPacketObserver(&s_packetObserver); +} + ++ (void)deregisterPacketProcessing:(AgoraRtcEngineKit *)rtcEngineKit { + if (!rtcEngineKit) { + return; + } + + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)rtcEngineKit.getNativeHandle; + rtc_engine->registerPacketObserver(NULL); +} + +@end diff --git a/iOS/APIExample/Common/EntryViewController.swift b/iOS/APIExample/Common/EntryViewController.swift new file mode 100644 index 000000000..743156ad3 --- /dev/null +++ b/iOS/APIExample/Common/EntryViewController.swift @@ -0,0 +1,39 @@ +// +// WelcomeViewController.swift +// APIExample +// +// Created by 张乾泽 on 2020/7/22. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import Foundation +import UIKit + +class EntryViewController : UIViewController +{ + @IBOutlet weak var joinButton: AGButton! + @IBOutlet weak var channelTextField: AGTextField! + @IBOutlet weak var noteLabel: UILabel! + var note: String = "" + + //identifer of next view controller after press join button + var nextVCIdentifier: String = "" + + override func viewDidLoad() { + super.viewDidLoad() + noteLabel.text = note + } + + @IBAction func doJoinPressed(sender: AGButton) { + guard let channelName = channelTextField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: nextVCIdentifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName":channelName] + self.navigationController?.pushViewController(newViewController, animated: true) + } +} diff --git a/iOS/APIExample/Common/ExternalAudio/AudioController.h b/iOS/APIExample/Common/ExternalAudio/AudioController.h new file mode 100644 index 000000000..4149e80b9 --- /dev/null +++ b/iOS/APIExample/Common/ExternalAudio/AudioController.h @@ -0,0 +1,35 @@ +// +// AudioController.h +// AudioCapture +// +// Created by CavanSu on 10/11/2017. +// Copyright © 2017 Agora. All rights reserved. +// + +#import +#import +#import "AudioOptions.h" + +@class AudioController; +@protocol AudioControllerDelegate +@optional +- (void)audioController:(AudioController *)controller + didCaptureData:(unsigned char *)data + bytesLength:(int)bytesLength; +- (int)audioController:(AudioController *)controller + didRenderData:(unsigned char *)data + bytesLength:(int)bytesLength; +- (void)audioController:(AudioController *)controller + error:(OSStatus)error + info:(NSString *)info; +@end + + +@interface AudioController : NSObject +@property (nonatomic, weak) id delegate; + ++ (instancetype)audioController; +- (void)setUpAudioSessionWithSampleRate:(int)sampleRate channelCount:(int)channelCount audioCRMode:(AudioCRMode)audioCRMode IOType:(IOUnitType)ioType; +- (void)startWork; +- (void)stopWork; + @end diff --git a/iOS/APIExample/Common/ExternalAudio/AudioController.m b/iOS/APIExample/Common/ExternalAudio/AudioController.m new file mode 100644 index 000000000..1cd84fa85 --- /dev/null +++ b/iOS/APIExample/Common/ExternalAudio/AudioController.m @@ -0,0 +1,417 @@ +// +// AudioController.m +// AudioCapture +// +// Created by CavanSu on 10/11/2017. +// Copyright © 2017 Agora. All rights reserved. +// + +#import "AudioController.h" +#import "AudioWriteToFile.h" + +#define InputBus 1 +#define OutputBus 0 + +@interface AudioController () +@property (nonatomic, assign) int sampleRate; +@property (nonatomic, assign) int channelCount; +@property (nonatomic, assign) AudioCRMode audioCRMode; +@property (nonatomic, assign) OSStatus error; + +@property (nonatomic, assign) AudioUnit remoteIOUnit; +#if TARGET_OS_MAC +@property (nonatomic, assign) AudioUnit macPlayUnit; +#endif +@end + +@implementation AudioController + +#if TARGET_OS_IPHONE +static double preferredIOBufferDuration = 0.02; +#endif + ++ (instancetype)audioController { + AudioController *audioController = [[self alloc] init]; + return audioController; +} + +#pragma mark - +static OSStatus captureCallBack(void *inRefCon, + AudioUnitRenderActionFlags *ioActionFlags, + const AudioTimeStamp *inTimeStamp, + UInt32 inBusNumber, // inputBus = 1 + UInt32 inNumberFrames, + AudioBufferList *ioData) +{ + AudioController *audioController = (__bridge AudioController *)inRefCon; + + AudioUnit captureUnit = [audioController remoteIOUnit]; + + if (!inRefCon) return 0; + + AudioBuffer buffer; + buffer.mData = NULL; + buffer.mDataByteSize = 0; + buffer.mNumberChannels = audioController.channelCount; + + AudioBufferList bufferList; + bufferList.mNumberBuffers = 1; + bufferList.mBuffers[0] = buffer; + + OSStatus status = AudioUnitRender(captureUnit, + ioActionFlags, + inTimeStamp, + inBusNumber, + inNumberFrames, + &bufferList); + + if (!status) { + if ([audioController.delegate respondsToSelector:@selector(audioController:didCaptureData:bytesLength:)]) { + [audioController.delegate audioController:audioController didCaptureData:(unsigned char *)bufferList.mBuffers[0].mData bytesLength:bufferList.mBuffers[0].mDataByteSize]; + } + } + else { + [audioController error:status position:@"captureCallBack"]; + } + + return 0; +} + +#pragma mark - +static OSStatus renderCallBack(void *inRefCon, + AudioUnitRenderActionFlags *ioActionFlags, + const AudioTimeStamp *inTimeStamp, + UInt32 inBusNumber, + UInt32 inNumberFrames, + AudioBufferList *ioData) +{ + AudioController *audioController = (__bridge AudioController *)(inRefCon); + + if (*ioActionFlags == kAudioUnitRenderAction_OutputIsSilence) { + return noErr; + } + + int result = 0; + + if ([audioController.delegate respondsToSelector:@selector(audioController:didRenderData:bytesLength:)]) { + result = [audioController.delegate audioController:audioController didRenderData:(uint8_t*)ioData->mBuffers[0].mData bytesLength:ioData->mBuffers[0].mDataByteSize]; + } + + if (result == 0) { + *ioActionFlags = kAudioUnitRenderAction_OutputIsSilence; + ioData->mBuffers[0].mDataByteSize = 0; + } + + return noErr; +} + + +#pragma mark - +- (void)setUpAudioSessionWithSampleRate:(int)sampleRate channelCount:(int)channelCount audioCRMode:(AudioCRMode)audioCRMode IOType:(IOUnitType)ioType{ + if (_audioCRMode == AudioCRModeSDKCaptureSDKRender) { + return; + } + + self.audioCRMode = audioCRMode; + self.sampleRate = sampleRate; + self.channelCount = channelCount; + +#if TARGET_OS_IPHONE + AVAudioSession *audioSession = [AVAudioSession sharedInstance]; + NSUInteger sessionOption = AVAudioSessionCategoryOptionMixWithOthers; + sessionOption |= AVAudioSessionCategoryOptionAllowBluetooth; + + [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:sessionOption error:nil]; + [audioSession setMode:AVAudioSessionModeDefault error:nil]; + [audioSession setPreferredIOBufferDuration:preferredIOBufferDuration error:nil]; + NSError *error; + BOOL success = [audioSession setActive:YES error:&error]; + if (!success) { + NSLog(@" audioSession setActive:YES error:nil"); + } + if (error) { + NSLog(@" setUpAudioSessionWithSampleRate : %@", error.localizedDescription); + } +#endif + + [self setupRemoteIOWithIOType:ioType]; +} + +#pragma mark - +- (void)setupRemoteIOWithIOType:(IOUnitType)ioType { +#if TARGET_OS_IPHONE + // AudioComponentDescription + AudioComponentDescription remoteIODesc; + remoteIODesc.componentType = kAudioUnitType_Output; + remoteIODesc.componentSubType = ioType == IOUnitTypeVPIO ? kAudioUnitSubType_VoiceProcessingIO : kAudioUnitSubType_RemoteIO; + remoteIODesc.componentManufacturer = kAudioUnitManufacturer_Apple; + remoteIODesc.componentFlags = 0; + remoteIODesc.componentFlagsMask = 0; + AudioComponent remoteIOComponent = AudioComponentFindNext(NULL, &remoteIODesc); + _error = AudioComponentInstanceNew(remoteIOComponent, &_remoteIOUnit); + [self error:_error position:@"AudioComponentInstanceNew"]; +#endif + + if (_audioCRMode == AudioCRModeExterCaptureSDKRender || _audioCRMode == AudioCRModeExterCaptureExterRender) { + +#if !TARGET_OS_IPHONE + AudioComponentDescription remoteIODesc; + remoteIODesc.componentType = kAudioUnitType_Output; + remoteIODesc.componentSubType = kAudioUnitSubType_HALOutput; + remoteIODesc.componentManufacturer = kAudioUnitManufacturer_Apple; + remoteIODesc.componentFlags = 0; + remoteIODesc.componentFlagsMask = 0; + AudioComponent remoteIOComponent = AudioComponentFindNext(NULL, &remoteIODesc); + _error = AudioComponentInstanceNew(remoteIOComponent, &_remoteIOUnit); + [self error:_error position:@"AudioComponentInstanceNew"]; + _error = AudioUnitInitialize(_remoteIOUnit); + [self error:_error position:@"AudioUnitInitialize"]; +#endif + [self setupCapture]; + } + + if (_audioCRMode == AudioCRModeSDKCaptureExterRender || _audioCRMode == AudioCRModeExterCaptureExterRender) { + +#if !TARGET_OS_IPHONE + AudioComponentDescription macPlayDesc; + macPlayDesc.componentType = kAudioUnitType_Output; + macPlayDesc.componentSubType = kAudioUnitSubType_DefaultOutput; + macPlayDesc.componentManufacturer = kAudioUnitManufacturer_Apple; + macPlayDesc.componentFlags = 0; + macPlayDesc.componentFlagsMask = 0; + AudioComponent macPlayComponent = AudioComponentFindNext(NULL, &macPlayDesc); + _error = AudioComponentInstanceNew(macPlayComponent, &_macPlayUnit); + [self error:_error position:@"AudioComponentInstanceNew"]; + _error = AudioUnitInitialize(_macPlayUnit); + [self error:_error position:@"AudioUnitInitialize"]; +#endif + [self setupRender]; + } + +} + +- (void)setupCapture { + // EnableIO + UInt32 one = 1; + _error = AudioUnitSetProperty(_remoteIOUnit, + kAudioOutputUnitProperty_EnableIO, + kAudioUnitScope_Input, + InputBus, + &one, + sizeof(one)); + [self error:_error position:@"kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input"]; + +#if !TARGET_OS_IPHONE + UInt32 disableFlag = 0; + + // Attention! set kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, disable + _error = AudioUnitSetProperty(_remoteIOUnit, + kAudioOutputUnitProperty_EnableIO, + kAudioUnitScope_Output, + OutputBus, + &disableFlag, + sizeof(disableFlag)); + [self error:_error position:@"kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output"]; + + AudioDeviceID defaultDevice = kAudioDeviceUnknown; + UInt32 propertySize = sizeof(defaultDevice); + AudioObjectPropertyAddress defaultDeviceProperty = { + .mSelector = kAudioHardwarePropertyDefaultInputDevice, + .mScope = kAudioObjectPropertyScopeInput, + .mElement = kAudioObjectPropertyElementMaster + }; + + _error = AudioObjectGetPropertyData(kAudioObjectSystemObject, + &defaultDeviceProperty, + 0, + NULL, + &propertySize, + &defaultDevice); + [self error:_error position:@"AudioObjectGetPropertyData, kAudioObjectSystemObject"]; + + // Set the sample rate of the input device to the output samplerate (if possible) + Float64 temp = _sampleRate; + defaultDeviceProperty.mSelector = kAudioDevicePropertyNominalSampleRate; + + _error = AudioObjectSetPropertyData(defaultDevice, + &defaultDeviceProperty, + 0, + NULL, + sizeof(Float64), + &temp); + [self error:_error position:@"AudioObjectSetPropertyData, defaultDeviceProperty"]; + + // Set the input device to the system's default input device + _error = AudioUnitSetProperty(_remoteIOUnit, + kAudioOutputUnitProperty_CurrentDevice, + kAudioUnitScope_Global, + InputBus, + &defaultDevice, + sizeof(defaultDevice)); + [self error:_error position:@"kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global"]; + +#endif + + // AudioStreamBasicDescription + AudioStreamBasicDescription streamFormatDesc = [self signedIntegerStreamFormatDesc]; + _error = AudioUnitSetProperty(_remoteIOUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Output, + InputBus, + &streamFormatDesc, + sizeof(streamFormatDesc)); + [self error:_error position:@"kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output"]; + + // CallBack + AURenderCallbackStruct captureCallBackStruck; + captureCallBackStruck.inputProcRefCon = (__bridge void * _Nullable)(self); + captureCallBackStruck.inputProc = captureCallBack; + + _error = AudioUnitSetProperty(_remoteIOUnit, + kAudioOutputUnitProperty_SetInputCallback, + kAudioUnitScope_Global, + InputBus, + &captureCallBackStruck, + sizeof(captureCallBackStruck)); + [self error:_error position:@"kAudioOutputUnitProperty_SetInputCallback"]; +} + +- (void)setupRender { + +#if TARGET_OS_IPHONE + // EnableIO + UInt32 one = 1; + _error = AudioUnitSetProperty(_remoteIOUnit, + kAudioOutputUnitProperty_EnableIO, + kAudioUnitScope_Output, + OutputBus, + &one, + sizeof(one)); + [self error:_error position:@"kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output"]; + + // AudioStreamBasicDescription + AudioStreamBasicDescription streamFormatDesc = [self signedIntegerStreamFormatDesc]; + _error = AudioUnitSetProperty(_remoteIOUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, + OutputBus, + &streamFormatDesc, + sizeof(streamFormatDesc)); + [self error:_error position:@"kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input"]; + + // CallBack + AURenderCallbackStruct renderCallback; + renderCallback.inputProcRefCon = (__bridge void * _Nullable)(self); + renderCallback.inputProc = renderCallBack; + AudioUnitSetProperty(_remoteIOUnit, + kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, + OutputBus, + &renderCallback, + sizeof(renderCallback)); + [self error:_error position:@"kAudioUnitProperty_SetRenderCallback"]; + +#else + + // AudioStreamBasicDescription + AudioStreamBasicDescription streamFormatDesc = [self signedIntegerStreamFormatDesc]; + _error = AudioUnitSetProperty(_macPlayUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, + OutputBus, + &streamFormatDesc, + sizeof(streamFormatDesc)); + [self error:_error position:@"kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input"]; + + // CallBack + AURenderCallbackStruct renderCallback; + renderCallback.inputProcRefCon = (__bridge void * _Nullable)(self); + renderCallback.inputProc = renderCallBack; + _error = AudioUnitSetProperty(_macPlayUnit, + kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, + OutputBus, + &renderCallback, + sizeof(renderCallback)); + [self error:_error position:@"kAudioUnitProperty_SetRenderCallback"]; +#endif + +} + +- (void)startWork { +#if TARGET_OS_IPHONE + _error = AudioOutputUnitStart(_remoteIOUnit); + [self error:_error position:@"AudioOutputUnitStart"]; +#else + if (_audioCRMode == AudioCRModeExterCaptureSDKRender || _audioCRMode == AudioCRModeExterCaptureExterRender) { + _error = AudioOutputUnitStart(_remoteIOUnit); + if (_error != noErr) { + [self error:_error position:@"AudioOutputUnitStart"]; + return; + } + } + + if (self.audioCRMode == AudioCRModeExterCaptureExterRender || self.audioCRMode == AudioCRModeSDKCaptureExterRender) { + _error = AudioOutputUnitStart(_macPlayUnit); + [self error:_error position:@"AudioOutputUnitStart"]; + } +#endif +} + +- (void)stopWork { +#if TARGET_OS_IPHONE + AudioOutputUnitStop(_remoteIOUnit); +#else + if (_audioCRMode == AudioCRModeExterCaptureSDKRender || _audioCRMode == AudioCRModeExterCaptureExterRender) { + AudioOutputUnitStop(_remoteIOUnit); + } + + if (self.audioCRMode == AudioCRModeExterCaptureExterRender || self.audioCRMode == AudioCRModeSDKCaptureExterRender) { + AudioOutputUnitStop(_macPlayUnit); + } +#endif +} + +- (void)error:(OSStatus)error position:(NSString *)position { + if (error != noErr) { + NSString *errorInfo = [NSString stringWithFormat:@" Error: %d, Position: %@", (int)error, position]; + if ([self.delegate respondsToSelector:@selector(audioController:error:info:)]) { + [self.delegate audioController:self error:error info:position]; + } + NSLog(@" :%@", errorInfo); + } +} + +- (AudioStreamBasicDescription)signedIntegerStreamFormatDesc { + AudioStreamBasicDescription streamFormatDesc; + streamFormatDesc.mSampleRate = _sampleRate; + streamFormatDesc.mFormatID = kAudioFormatLinearPCM; + streamFormatDesc.mFormatFlags = (kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked); + streamFormatDesc.mChannelsPerFrame = _channelCount; + streamFormatDesc.mFramesPerPacket = 1; + streamFormatDesc.mBitsPerChannel = 16; + streamFormatDesc.mBytesPerFrame = streamFormatDesc.mBitsPerChannel / 8 * streamFormatDesc.mChannelsPerFrame; + streamFormatDesc.mBytesPerPacket = streamFormatDesc.mBytesPerFrame * streamFormatDesc.mFramesPerPacket; + + return streamFormatDesc; +} + +- (void)dealloc { + if (_remoteIOUnit) { + AudioOutputUnitStop(_remoteIOUnit); + AudioComponentInstanceDispose(_remoteIOUnit); + _remoteIOUnit = nil; + } + +#if !TARGET_OS_IPHONE + if (_macPlayUnit) { + AudioOutputUnitStop(_macPlayUnit); + AudioComponentInstanceDispose(_macPlayUnit); + _macPlayUnit = nil; + } +#endif + + NSLog(@" AudioController dealloc"); +} + +@end diff --git a/iOS/APIExample/Common/ExternalAudio/AudioOptions.h b/iOS/APIExample/Common/ExternalAudio/AudioOptions.h new file mode 100644 index 000000000..0a40ef9cc --- /dev/null +++ b/iOS/APIExample/Common/ExternalAudio/AudioOptions.h @@ -0,0 +1,40 @@ +// +// AudioOptions.h +// AgoraAudioIO +// +// Created by CavanSu on 12/03/2018. +// Copyright © 2018 CavanSu. All rights reserved. +// + +#ifndef AudioOptions_h +#define AudioOptions_h + +typedef NS_ENUM(int, AudioCRMode) { + AudioCRModeExterCaptureSDKRender = 1, + AudioCRModeSDKCaptureExterRender = 2, + AudioCRModeSDKCaptureSDKRender = 3, + AudioCRModeExterCaptureExterRender = 4 +}; + +typedef NS_ENUM(int, IOUnitType) { + IOUnitTypeVPIO, + IOUnitTypeRemoteIO +}; + +typedef NS_ENUM(int, ChannelMode) { + ChannelModeCommunication = 0, + ChannelModeLiveBroadcast = 1 +}; + +typedef NS_ENUM(int, ClientRole) { + ClientRoleAudience = 0, + ClientRoleBroadcast = 1 +}; + +#if TARGET_OS_IPHONE +#import "UIColor+CSRGB.h" +#import "UIView+CSshortFrame.h" +#define ThemeColor [UIColor Red: 122 Green: 203 Blue: 253] +#endif + +#endif /* AudioOptions_h */ diff --git a/iOS/APIExample/Common/ExternalAudio/AudioWriteToFile.h b/iOS/APIExample/Common/ExternalAudio/AudioWriteToFile.h new file mode 100644 index 000000000..9ccf24b14 --- /dev/null +++ b/iOS/APIExample/Common/ExternalAudio/AudioWriteToFile.h @@ -0,0 +1,13 @@ +// +// AudioWriteToFile.h +// AudioCapture +// +// Created by CavanSu on 08/11/2017. +// Copyright © 2017 Agora. All rights reserved. +// + +#import + +@interface AudioWriteToFile : NSObject ++ (void)writeToFileWithData:(void *)data length:(int)bytes; +@end diff --git a/iOS/APIExample/Common/ExternalAudio/AudioWriteToFile.m b/iOS/APIExample/Common/ExternalAudio/AudioWriteToFile.m new file mode 100644 index 000000000..54558635a --- /dev/null +++ b/iOS/APIExample/Common/ExternalAudio/AudioWriteToFile.m @@ -0,0 +1,39 @@ +// +// AudioWriteToFile.m +// AudioCapture +// +// Created by CavanSu on 08/11/2017. +// Copyright © 2017 Agora. All rights reserved. +// + +#import "AudioWriteToFile.h" + +@implementation AudioWriteToFile + +static NSFileHandle *file = nil; +static dispatch_queue_t queue = nil; + ++ (void)load { + queue = dispatch_queue_create("writeFile", NULL); +} + ++ (void)writeToFileWithData:(void *)data length:(int)bytes { + if(NULL == data || bytes < 1) return; + + dispatch_async(queue, ^{ + + if (file == nil) { + NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"1.pcm"]; + [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; + if (![[NSFileManager defaultManager] createFileAtPath:path contents:nil attributes:nil]) { + + } + else { + file = [NSFileHandle fileHandleForWritingAtPath:path]; + } + } + [file writeData:[NSData dataWithBytes:data length:bytes]]; + }); +} + +@end diff --git a/iOS/APIExample/Common/ExternalAudio/ExternalAudio.h b/iOS/APIExample/Common/ExternalAudio/ExternalAudio.h new file mode 100644 index 000000000..17e1cb3a1 --- /dev/null +++ b/iOS/APIExample/Common/ExternalAudio/ExternalAudio.h @@ -0,0 +1,26 @@ +// +// ExternalAudio.h +// AgoraAudioIO +// +// Created by CavanSu on 22/01/2018. +// Copyright © 2018 CavanSu. All rights reserved. +// + +#import +#import "AudioOptions.h" + +@class AgoraRtcEngineKit; +@class ExternalAudio; +@protocol ExternalAudioDelegate +@optional +- (void)externalAudio:(ExternalAudio *)externalAudio errorInfo:(NSString *)errorInfo; +@end + +@interface ExternalAudio : NSObject +@property (nonatomic, weak) id delegate; + ++ (instancetype)sharedExternalAudio; +- (void)setupExternalAudioWithAgoraKit:(AgoraRtcEngineKit *)agoraKit sampleRate:(uint)sampleRate channels:(uint)channels audioCRMode:(AudioCRMode)audioCRMode IOType:(IOUnitType)ioType; +- (void)startWork; +- (void)stopWork; +@end diff --git a/iOS/APIExample/Common/ExternalAudio/ExternalAudio.mm b/iOS/APIExample/Common/ExternalAudio/ExternalAudio.mm new file mode 100644 index 000000000..04bae4402 --- /dev/null +++ b/iOS/APIExample/Common/ExternalAudio/ExternalAudio.mm @@ -0,0 +1,310 @@ +// +// ExternalAudio.m +// AgoraAudioIO +// +// Created by CavanSu on 22/01/2018. +// Copyright © 2018 CavanSu. All rights reserved. +// + +#import "ExternalAudio.h" +#import "AudioController.h" +#import "AudioWriteToFile.h" + +#if TARGET_OS_IPHONE +#import +#import +#import +#else +#import +#import +#import +#endif + +@interface ExternalAudio () +@property (nonatomic, strong) AudioController *audioController; +@property (nonatomic, assign) AudioCRMode audioCRMode; +@property (nonatomic, assign) int sampleRate; +@property (nonatomic, assign) int channelCount; +@property (nonatomic, weak) AgoraRtcEngineKit *agoraKit; +@end + +@implementation ExternalAudio + +static NSObject *threadLockCapture; +static NSObject *threadLockPlay; + +#pragma mark - C++ ExternalAudioFrameObserver +class ExternalAudioFrameObserver : public agora::media::IAudioFrameObserver +{ +private: + + // total buffer length of per second + enum { kBufferLengthBytes = 441 * 2 * 2 * 50 }; // + + // capture + char byteBuffer[kBufferLengthBytes]; // char take up 1 byte, byterBuffer[] take up 88200 bytes + int readIndex = 0; + int writeIndex = 0; + int availableBytes = 0; + int channels = 1; + + // play + char byteBuffer_play[kBufferLengthBytes]; + int readIndex_play = 0; + int writeIndex_play = 0; + int availableBytes_play = 0; + int channels_play = 1; + +public: + int sampleRate = 0; + int sampleRate_play = 0; + + bool isExternalCapture = false; + bool isExternalRender = false; + +#pragma mark- + // push audio data to special buffer(Array byteBuffer) + // bytesLength = date length + void pushExternalData(void* data, int bytesLength) + { + @synchronized(threadLockCapture) { + + if (availableBytes + bytesLength > kBufferLengthBytes) { + + readIndex = 0; + writeIndex = 0; + availableBytes = 0; + } + + if (writeIndex + bytesLength > kBufferLengthBytes) { + + int left = kBufferLengthBytes - writeIndex; + memcpy(byteBuffer + writeIndex, data, left); + memcpy(byteBuffer, (char *)data + left, bytesLength - left); + writeIndex = bytesLength - left; + } + else { + + memcpy(byteBuffer + writeIndex, data, bytesLength); + writeIndex += bytesLength; + } + availableBytes += bytesLength; + } + + } + + // copy byteBuffer to audioFrame.buffer + virtual bool onRecordAudioFrame(AudioFrame& audioFrame) override + { + @synchronized(threadLockCapture) { + + if (isExternalCapture == false) return true; + + int readBytes = sampleRate / 100 * channels * audioFrame.bytesPerSample; + + if (availableBytes < readBytes) { + return false; + } + + audioFrame.samplesPerSec = sampleRate; + unsigned char tmp[960]; // The most rate:@48k fs, channels = 1, the most total size = 960; + + if (readIndex + readBytes > kBufferLengthBytes) { + int left = kBufferLengthBytes - readIndex; + memcpy(tmp, byteBuffer + readIndex, left); + memcpy(tmp + left, byteBuffer, readBytes - left); + readIndex = readBytes - left; + } + else { + memcpy(tmp, byteBuffer + readIndex, readBytes); + readIndex += readBytes; + } + + availableBytes -= readBytes; + + if (channels == audioFrame.channels) { + memcpy(audioFrame.buffer, tmp, readBytes); + } + [AudioWriteToFile writeToFileWithData:audioFrame.buffer length:readBytes]; + return true; + } + + } + +#pragma mark- + // read Audio data from byteBuffer_play to audioUnit + int readAudioData(void* data, int bytesLength) + { + @synchronized(threadLockPlay) { + + if (NULL == data || bytesLength < 1 || availableBytes_play < bytesLength) { + return 0; + } + + int readBytes = bytesLength; + + unsigned char tmp[4096]; // unsigned char takes up 1 byte + + if (readIndex_play + readBytes > kBufferLengthBytes) { + + int left = kBufferLengthBytes - readIndex_play; + memcpy(tmp, byteBuffer_play + readIndex_play, left); + memcpy(tmp + left, byteBuffer_play, readBytes - left); + readIndex_play = readBytes - left; + } + else { + + memcpy(tmp, byteBuffer_play + readIndex_play, readBytes); + readIndex_play += readBytes; + } + + availableBytes_play -= readBytes; + + if (channels_play == 1) { + memcpy(data, tmp, readBytes); + } + + [AudioWriteToFile writeToFileWithData:data length:readBytes]; + + return readBytes; + } + + } + + // recive remote audio stream, push audio data to byteBuffer_play + virtual bool onPlaybackAudioFrame(AudioFrame& audioFrame) override + { + @synchronized(threadLockPlay) { + + if (isExternalRender == false) return true; + + int bytesLength = audioFrame.samples * audioFrame.channels * audioFrame.bytesPerSample; + char *data = (char *)audioFrame.buffer; + + sampleRate_play = audioFrame.samplesPerSec; + channels_play = audioFrame.channels; + + if (availableBytes_play + bytesLength > kBufferLengthBytes) { + + readIndex_play = 0; + writeIndex_play = 0; + availableBytes_play = 0; + } + + if (writeIndex_play + bytesLength > kBufferLengthBytes) { + + int left = kBufferLengthBytes - writeIndex_play; + memcpy(byteBuffer_play + writeIndex_play, data, left); + memcpy(byteBuffer_play, (char *)data + left, bytesLength - left); + writeIndex_play = bytesLength - left; + } + else { + + memcpy(byteBuffer_play + writeIndex_play, data, bytesLength); + writeIndex_play += bytesLength; + } + + availableBytes_play += bytesLength; + + return true; + } + + } + + virtual bool onPlaybackAudioFrameBeforeMixing(unsigned int uid, AudioFrame& audioFrame) override { return true; } + + virtual bool onMixedAudioFrame(AudioFrame& audioFrame) override { return true; } +}; + +static ExternalAudioFrameObserver* s_audioFrameObserver; + + ++ (instancetype)sharedExternalAudio { + ExternalAudio *audio = [[ExternalAudio alloc] init]; + return audio; +} + +- (void)setupExternalAudioWithAgoraKit:(AgoraRtcEngineKit *)agoraKit sampleRate:(uint)sampleRate channels:(uint)channels audioCRMode:(AudioCRMode)audioCRMode IOType:(IOUnitType)ioType { + + threadLockCapture = [[NSObject alloc] init]; + threadLockPlay = [[NSObject alloc] init]; + + // AudioController + self.audioController = [AudioController audioController]; + self.audioController.delegate = self; + [self.audioController setUpAudioSessionWithSampleRate:sampleRate channelCount:channels audioCRMode:audioCRMode IOType:ioType]; + + // Agora Engine of C++ + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)agoraKit.getNativeHandle; + agora::util::AutoPtr mediaEngine; + mediaEngine.queryInterface(rtc_engine, agora::AGORA_IID_MEDIA_ENGINE); + + if (mediaEngine) { + s_audioFrameObserver = new ExternalAudioFrameObserver(); + s_audioFrameObserver -> sampleRate = sampleRate; + s_audioFrameObserver -> sampleRate_play = channels; + mediaEngine->registerAudioFrameObserver(s_audioFrameObserver); + } + + if (audioCRMode == AudioCRModeExterCaptureExterRender || audioCRMode == AudioCRModeSDKCaptureExterRender) { + s_audioFrameObserver -> isExternalRender = true; + } + if (audioCRMode == AudioCRModeExterCaptureExterRender || audioCRMode == AudioCRModeExterCaptureSDKRender) { + s_audioFrameObserver -> isExternalCapture = true; + } + + self.agoraKit = agoraKit; + self.audioCRMode = audioCRMode; +} + +- (void)startWork { + [self.audioController startWork]; +} + +- (void)stopWork { + [self.audioController stopWork]; + [self cancelRegiset]; +} + +- (void)cancelRegiset { + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)self.agoraKit.getNativeHandle; + agora::util::AutoPtr mediaEngine; + mediaEngine.queryInterface(rtc_engine, agora::AGORA_IID_MEDIA_ENGINE); + mediaEngine->registerAudioFrameObserver(NULL); +} + +- (void)audioController:(AudioController *)controller didCaptureData:(unsigned char *)data bytesLength:(int)bytesLength { + + if (self.audioCRMode != AudioCRModeExterCaptureSDKRender) { + if (s_audioFrameObserver) { + s_audioFrameObserver -> pushExternalData(data, bytesLength); + } + } + else { + [self.agoraKit pushExternalAudioFrameRawData:data samples:bytesLength / 2 timestamp:0]; + } + +} + +- (int)audioController:(AudioController *)controller didRenderData:(unsigned char *)data bytesLength:(int)bytesLength { + int result = 0; + + if (s_audioFrameObserver) { + result = s_audioFrameObserver -> readAudioData(data, bytesLength); + } + + return result; +} + +- (void)audioController:(AudioController *)controller error:(OSStatus)error info:(NSString *)info { + if ([self.delegate respondsToSelector:@selector(externalAudio:errorInfo:)]) { + NSString *errorInfo = [NSString stringWithFormat:@" error:%d, info:%@", error, info]; + [self.delegate externalAudio:self errorInfo:errorInfo]; + } +} + +- (void)dealloc { + NSLog(@"ExAudio dealloc"); +} + +@end diff --git a/iOS/APIExample/Common/ExternalAudio/UIColor+CSRGB.h b/iOS/APIExample/Common/ExternalAudio/UIColor+CSRGB.h new file mode 100755 index 000000000..5027c3e5d --- /dev/null +++ b/iOS/APIExample/Common/ExternalAudio/UIColor+CSRGB.h @@ -0,0 +1,12 @@ +// +// UIColor+CSRGB.h +// +// Created by CavanSu on 17/3/3. +// Copyright © 2017 CavanSu. All rights reserved. +// + +#import + +@interface UIColor (CSRGB) ++ (UIColor*)Red:(CGFloat)red Green:(CGFloat)green Blue:(CGFloat)blue; +@end diff --git a/iOS/APIExample/Common/ExternalAudio/UIColor+CSRGB.m b/iOS/APIExample/Common/ExternalAudio/UIColor+CSRGB.m new file mode 100755 index 000000000..2c454a76b --- /dev/null +++ b/iOS/APIExample/Common/ExternalAudio/UIColor+CSRGB.m @@ -0,0 +1,15 @@ +// +// UIColor+CSRGB.m +// +// Created by CavanSu on 17/3/3. +// Copyright © 2017 CavanSu. All rights reserved. +// + +#import "UIColor+CSRGB.h" + +@implementation UIColor (CSRGB) ++ (UIColor*)Red:(CGFloat)red Green:(CGFloat)green Blue:(CGFloat)blue { + UIColor *color = [UIColor colorWithRed:red / 255 green:green / 255 blue:blue / 255 alpha: 1]; + return color; +} +@end diff --git a/iOS/APIExample/Common/ExternalAudio/UIView+CSshortFrame.h b/iOS/APIExample/Common/ExternalAudio/UIView+CSshortFrame.h new file mode 100755 index 000000000..f0b6e43ed --- /dev/null +++ b/iOS/APIExample/Common/ExternalAudio/UIView+CSshortFrame.h @@ -0,0 +1,19 @@ +// +// UIView+CSshortFrame.h +// +// Created by CavanSu on 23/02/2017. +// Copyright © 2017 CavanSu. All rights reserved. +// + +#import + +@interface UIView (CSshortFrame) +@property (assign, nonatomic) CGFloat x_CS; +@property (assign, nonatomic) CGFloat y_CS; +@property (assign, nonatomic) CGFloat width_CS; +@property (assign, nonatomic) CGFloat height_CS; +@property (assign, nonatomic) CGSize size_CS; +@property (assign, nonatomic) CGPoint origin_CS; +@property (assign, nonatomic) CGFloat centerX_CS; +@property (assign, nonatomic) CGFloat centerY_CS; +@end diff --git a/iOS/APIExample/Common/ExternalAudio/UIView+CSshortFrame.m b/iOS/APIExample/Common/ExternalAudio/UIView+CSshortFrame.m new file mode 100755 index 000000000..b7ebd834d --- /dev/null +++ b/iOS/APIExample/Common/ExternalAudio/UIView+CSshortFrame.m @@ -0,0 +1,90 @@ +// +// UIView+CSshortFrame.m +// +// Created by CavanSu on 23/02/2017. +// Copyright © 2017 CavanSu. All rights reserved. +// + +#import "UIView+CSshortFrame.h" + +@implementation UIView (CSshortFrame) +-(void)setX_CS:(CGFloat)x_CS { + CGRect frame = self.frame; + frame.origin.x = x_CS; + self.frame = frame; +} + +-(CGFloat)x_CS { + return self.frame.origin.x; +} + +-(void)setY_CS:(CGFloat)y_CS { + CGRect frame = self.frame; + frame.origin.y = y_CS; + self.frame = frame; +} + +-(CGFloat)y_CS { + return self.frame.origin.y; +} + +-(void)setWidth_CS:(CGFloat)width_CS { + CGRect frame = self.frame; + frame.size.width = width_CS; + self.frame = frame; +} + +-(CGFloat)width_CS { + return self.frame.size.width; +} + +-(void)setHeight_CS:(CGFloat)height_CS { + CGRect frame = self.frame; + frame.size.height = height_CS; + self.frame = frame; +} + +-(CGFloat)height_CS { + return self.frame.size.height; +} + +-(void)setSize_CS:(CGSize)size_CS { + CGRect frame = self.frame; + frame.size = size_CS; + self.frame = frame; +} + +-(CGSize)size_CS { + return self.frame.size; +} + +-(void)setOrigin_CS:(CGPoint)origin_CS { + CGRect frame = self.frame; + frame.origin = origin_CS; + self.frame = frame; +} + +-(CGPoint)origin_CS { + return self.frame.origin; +} + +-(void)setCenterX_CS:(CGFloat)centerX_CS { + CGPoint center = self.center; + center.x = centerX_CS; + self.center = center; +} + +-(CGFloat)centerX_CS { + return self.center.x; +} + +-(void)setCenterY_CS:(CGFloat)centerY_CS { + CGPoint center = self.center; + center.y = centerY_CS; + self.center = center; +} + +-(CGFloat)centerY_CS { + return self.center.y; +} +@end diff --git a/iOS/APIExample/Common/ExternalVideo/AgoraCameraSourceMediaIO.swift b/iOS/APIExample/Common/ExternalVideo/AgoraCameraSourceMediaIO.swift new file mode 100644 index 000000000..59a9dd2ea --- /dev/null +++ b/iOS/APIExample/Common/ExternalVideo/AgoraCameraSourceMediaIO.swift @@ -0,0 +1,217 @@ +// +// AgoraCamera.swift +// Agora-Custom-Media-Device +// +// Created by GongYuhua on 2017/11/10. +// Copyright © 2017年 Agora.io All rights reserved. +// + + +import UIKit +import AgoraRtcKit + +extension AVCaptureDevice.Position { + func reverse() -> AVCaptureDevice.Position { + switch self { + case .front: return .back + case .back, .unspecified: return .front + default: return .front + } + } + + func isFront() -> Bool { + return self == .front + } +} + +class AgoraCameraSourceMediaIO: NSObject { + var consumer: AgoraVideoFrameConsumer? + + var isFront: Bool { + get { + return position.isFront() + } + } + + private var viewOrientation = UIInterfaceOrientation.portrait + private var viewOrientationObserver: NSObjectProtocol? + private var position = AVCaptureDevice.Position.front + private var captureSession: AVCaptureSession? + private var captureQueue: DispatchQueue? + private var currentOutput: AVCaptureVideoDataOutput? { + if let outputs = self.captureSession?.outputs as? [AVCaptureVideoDataOutput] { + return outputs.first + } else { + return nil + } + } + + override init() { + super.init() + viewOrientationObserver = NotificationCenter.default.addObserver(forName: UIApplication.willChangeStatusBarOrientationNotification, object: nil, queue: nil) { [weak self] (notify) in + guard let value = notify.userInfo?[UIApplication.statusBarOrientationUserInfoKey] as? Int, let orientation = UIInterfaceOrientation(rawValue: value) else { + return + } + self?.viewOrientation = orientation + } + } + + deinit { + if let viewOrientationObserver = viewOrientationObserver { + NotificationCenter.default.removeObserver(viewOrientationObserver) + self.viewOrientationObserver = nil + } + } + + func switchCamera() { + position = position.reverse() + captureQueue?.async { [weak self] in + guard let strongSelf = self, let captureSession = strongSelf.captureSession, captureSession.isRunning else { + return + } + + strongSelf.stopCapture() + strongSelf.startCapture() + } + } +} + +private extension AgoraCameraSourceMediaIO { + func initialize() -> Bool { + let captureSession = AVCaptureSession() + captureSession.usesApplicationAudioSession = false + let captureOutput = AVCaptureVideoDataOutput() + captureOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange] + if captureSession.canAddOutput(captureOutput) { + captureSession.addOutput(captureOutput) + } + + self.captureSession = captureSession + captureQueue = DispatchQueue(label: "Agora-Custom-Video-Capture-Queue") + + return true + } + + func startCapture() { + guard let currentOutput = currentOutput, let captureQueue = captureQueue else { + return + } + + currentOutput.setSampleBufferDelegate(self, queue: captureQueue) + captureQueue.async { [weak self] in + guard let strongSelf = self, let captureSession = strongSelf.captureSession else { + return + } + strongSelf.changeCaptureDevice(toPosition: strongSelf.position, ofSession: captureSession) + captureSession.beginConfiguration() + if captureSession.canSetSessionPreset(.vga640x480) { + captureSession.sessionPreset = .vga640x480 + } + captureSession.commitConfiguration() + captureSession.startRunning() + } + } + + func stopCapture() { + currentOutput?.setSampleBufferDelegate(nil, queue: nil) + captureQueue?.async { [weak self] in + self?.captureSession?.stopRunning() + } + } + + func dispose() { + captureQueue = nil + captureSession = nil + } +} + +private extension AgoraCameraSourceMediaIO { + func changeCaptureDevice(toPosition position: AVCaptureDevice.Position, ofSession captureSession: AVCaptureSession) { + guard let captureDevice = captureDevice(atPosition: position) else { + return + } + + let currentInputs = captureSession.inputs as? [AVCaptureDeviceInput] + let currentInput = currentInputs?.first + + if let currentInput = currentInput, currentInput.device.localizedName == captureDevice.uniqueID { + return + } + + guard let newInput = try? AVCaptureDeviceInput(device: captureDevice) else { + return + } + + captureSession.beginConfiguration() + if let currentInput = currentInput { + captureSession.removeInput(currentInput) + } + if captureSession.canAddInput(newInput) { + captureSession.addInput(newInput) + } + captureSession.commitConfiguration() + } + + func captureDevice(atPosition position: AVCaptureDevice.Position) -> AVCaptureDevice? { + let devices = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: position).devices + return devices.first + } +} + +extension AgoraCameraSourceMediaIO: AgoraVideoSourceProtocol { + func shouldInitialize() -> Bool { + return initialize() + } + + func shouldStart() { + startCapture() + } + + func shouldStop() { + stopCapture() + } + + func shouldDispose() { + dispose() + } + + func bufferType() -> AgoraVideoBufferType { + return .pixelBuffer + } + + func contentHint() -> AgoraVideoContentHint { + return .none + } + + func captureType() -> AgoraVideoCaptureType { + return .camera + } +} + +extension AgoraCameraSourceMediaIO: AVCaptureVideoDataOutputSampleBufferDelegate { + func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { + guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer), CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) == kCVReturnSuccess else { + return + } + defer { + CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) + } + + let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + let rotation = viewOrientation.agoraRotation() + consumer?.consumePixelBuffer(pixelBuffer, withTimestamp: time, rotation: rotation) + } +} + +private extension UIInterfaceOrientation { + func agoraRotation() -> AgoraVideoRotation { + switch self { + case .portrait: return .rotation90 + case .portraitUpsideDown: return .rotation270 + case .landscapeLeft: return .rotationNone + case .landscapeRight: return .rotation180 + case .unknown: return .rotation90 + default: return .rotation90 + } + } +} diff --git a/iOS/APIExample/Common/ExternalVideo/AgoraCameraSourcePush.swift b/iOS/APIExample/Common/ExternalVideo/AgoraCameraSourcePush.swift new file mode 100644 index 000000000..fa2a93783 --- /dev/null +++ b/iOS/APIExample/Common/ExternalVideo/AgoraCameraSourcePush.swift @@ -0,0 +1,169 @@ +// +// MyVideoCapture.swift +// Agora-Video-Source +// +// Created by GongYuhua on 2017/4/11. +// Copyright © 2017年 Agora. All rights reserved. +// + +import UIKit +import AVFoundation + +protocol AgoraCameraSourcePushDelegate { + func myVideoCapture(_ capture: AgoraCameraSourcePush, didOutputSampleBuffer pixelBuffer: CVPixelBuffer, rotation: Int, timeStamp: CMTime) +} + +enum Camera: Int { + case front = 1 + case back = 0 + + static func defaultCamera() -> Camera { + return .front + } + + func next() -> Camera { + switch self { + case .back: return .front + case .front: return .back + } + } +} + +class AgoraCameraSourcePush: NSObject { + + fileprivate var delegate: AgoraCameraSourcePushDelegate? + private var videoView: CustomVideoSourcePreview + + private var currentCamera = Camera.defaultCamera() + private let captureSession: AVCaptureSession + private let captureQueue: DispatchQueue + private var currentOutput: AVCaptureVideoDataOutput? { + if let outputs = self.captureSession.outputs as? [AVCaptureVideoDataOutput] { + return outputs.first + } else { + return nil + } + } + + init(delegate: AgoraCameraSourcePushDelegate?, videoView: CustomVideoSourcePreview) { + self.delegate = delegate + self.videoView = videoView + + captureSession = AVCaptureSession() + captureSession.usesApplicationAudioSession = false + + let captureOutput = AVCaptureVideoDataOutput() + captureOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange] + if captureSession.canAddOutput(captureOutput) { + captureSession.addOutput(captureOutput) + } + + captureQueue = DispatchQueue(label: "MyCaptureQueue") + + let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + videoView.insertCaptureVideoPreviewLayer(previewLayer: previewLayer) + } + + deinit { + captureSession.stopRunning() + } + + func startCapture(ofCamera camera: Camera) { + guard let currentOutput = currentOutput else { + return + } + + currentCamera = camera + currentOutput.setSampleBufferDelegate(self, queue: captureQueue) + + captureQueue.async { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.changeCaptureDevice(toIndex: camera.rawValue, ofSession: strongSelf.captureSession) + strongSelf.captureSession.beginConfiguration() + if strongSelf.captureSession.canSetSessionPreset(AVCaptureSession.Preset.vga640x480) { + strongSelf.captureSession.sessionPreset = AVCaptureSession.Preset.vga640x480 + } + strongSelf.captureSession.commitConfiguration() + strongSelf.captureSession.startRunning() + } + } + + func stopCapture() { + currentOutput?.setSampleBufferDelegate(nil, queue: nil) + captureQueue.async { [weak self] in + self?.captureSession.stopRunning() + } + } + + func switchCamera() { + stopCapture() + currentCamera = currentCamera.next() + startCapture(ofCamera: currentCamera) + } +} + +private extension AgoraCameraSourcePush { + func changeCaptureDevice(toIndex index: Int, ofSession captureSession: AVCaptureSession) { + guard let captureDevice = captureDevice(atIndex: index) else { + return + } + + let currentInputs = captureSession.inputs as? [AVCaptureDeviceInput] + let currentInput = currentInputs?.first + + if let currentInputName = currentInput?.device.localizedName, + currentInputName == captureDevice.uniqueID { + return + } + + guard let newInput = try? AVCaptureDeviceInput(device: captureDevice) else { + return + } + + captureSession.beginConfiguration() + if let currentInput = currentInput { + captureSession.removeInput(currentInput) + } + if captureSession.canAddInput(newInput) { + captureSession.addInput(newInput) + } + captureSession.commitConfiguration() + } + + func captureDevice(atIndex index: Int) -> AVCaptureDevice? { + let deviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: AVMediaType.video, position: .back) + let devices = deviceDiscoverySession.devices + + let count = devices.count + guard count > 0, index >= 0 else { + return nil + } + + let device: AVCaptureDevice + if index >= count { + device = devices.last! + } else { + device = devices[index] + } + + return device + } +} + +extension AgoraCameraSourcePush: AVCaptureVideoDataOutputSampleBufferDelegate { + func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { + guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { + return + } + let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + DispatchQueue.main.async {[weak self] in + guard let weakSelf = self else { + return + } + + weakSelf.delegate?.myVideoCapture(weakSelf, didOutputSampleBuffer: pixelBuffer, rotation: 90, timeStamp: time) + } + } +} diff --git a/iOS/APIExample/Common/ExternalVideo/AgoraMetalRender.swift b/iOS/APIExample/Common/ExternalVideo/AgoraMetalRender.swift new file mode 100644 index 000000000..15e275c6a --- /dev/null +++ b/iOS/APIExample/Common/ExternalVideo/AgoraMetalRender.swift @@ -0,0 +1,304 @@ +// +// AgoraMetalRender.swift +// Agora-Custom-Media-Device +// +// Created by GongYuhua on 2017/11/15. +// Copyright © 2017年 Agora.io All rights reserved. +// + +import CoreMedia +import Metal +#if os(iOS) && (!arch(i386) && !arch(x86_64)) + import MetalKit +#endif +import AgoraRtcKit + +protocol AgoraMetalRenderMirrorDataSource: NSObjectProtocol { + func renderViewShouldMirror(renderView: AgoraMetalRender) -> Bool +} + +class AgoraMetalRender: UIView { + weak var mirrorDataSource: AgoraMetalRenderMirrorDataSource? + + fileprivate var textures: [MTLTexture]? + fileprivate var vertexBuffer: MTLBuffer? + fileprivate var viewSize = CGSize.zero + + fileprivate var device = MTLCreateSystemDefaultDevice() + fileprivate var renderPipelineState: MTLRenderPipelineState? + fileprivate let semaphore = DispatchSemaphore(value: 1) + fileprivate var metalDevice = MTLCreateSystemDefaultDevice() +#if os(iOS) && (!arch(i386) && !arch(x86_64)) + fileprivate var metalView: MTKView! + fileprivate var textureCache: CVMetalTextureCache? +#endif + fileprivate var commandQueue: MTLCommandQueue? + + init() { + super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + initializeMetalView() + initializeTextureCache() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + initializeMetalView() + initializeTextureCache() + } + + override init(frame frameRect: CGRect) { + super.init(frame: frameRect) + initializeMetalView() + initializeTextureCache() + } + + override var bounds: CGRect { + didSet { + viewSize = bounds.size + } + } +} + +extension AgoraMetalRender: AgoraVideoSinkProtocol { + func shouldInitialize() -> Bool { + initializeRenderPipelineState() + return true + } + + func shouldStart() { + #if os(iOS) && (!arch(i386) && !arch(x86_64)) + metalView.delegate = self + #endif + } + + func shouldStop() { + #if os(iOS) && (!arch(i386) && !arch(x86_64)) + metalView.delegate = nil + #endif + } + + func shouldDispose() { + _ = semaphore.wait(timeout: .distantFuture) + textures = nil + vertexBuffer = nil + #if os(macOS) || (os(iOS) && (!arch(i386) && !arch(x86_64))) + metalView.delegate = nil + #endif + semaphore.signal() + } + + func bufferType() -> AgoraVideoBufferType { + return .pixelBuffer + } + + func pixelFormat() -> AgoraVideoPixelFormat { + return .NV12 + } + + func renderPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: AgoraVideoRotation) { + #if os(iOS) && (!arch(i386) && !arch(x86_64)) + guard CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) == kCVReturnSuccess else { + return + } + defer { + CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) + } + + let isPlanar = CVPixelBufferIsPlanar(pixelBuffer) + let width = isPlanar ? CVPixelBufferGetWidthOfPlane(pixelBuffer, 0) : CVPixelBufferGetWidth(pixelBuffer) + let height = isPlanar ? CVPixelBufferGetHeightOfPlane(pixelBuffer, 0) : CVPixelBufferGetHeight(pixelBuffer) + let size = CGSize(width: width, height: height) + + let mirror = mirrorDataSource?.renderViewShouldMirror(renderView: self) ?? false + if let renderedCoordinates = rotation.renderedCoordinates(mirror: mirror, + videoSize: size, + viewSize: viewSize) { + let byteLength = 4 * MemoryLayout.size(ofValue: renderedCoordinates[0]) + vertexBuffer = device?.makeBuffer(bytes: renderedCoordinates, length: byteLength, options: [.storageModeShared]) + } + + if let yTexture = texture(pixelBuffer: pixelBuffer, textureCache: textureCache, planeIndex: 0, pixelFormat: .r8Unorm), + let uvTexture = texture(pixelBuffer: pixelBuffer, textureCache: textureCache, planeIndex: 1, pixelFormat: .rg8Unorm) { + self.textures = [yTexture, uvTexture] + } + #endif + } +} + +private extension AgoraMetalRender { + func initializeMetalView() { + #if os(iOS) && (!arch(i386) && !arch(x86_64)) + metalView = MTKView(frame: bounds, device: device) + metalView.framebufferOnly = true + metalView.colorPixelFormat = .bgra8Unorm + metalView.contentScaleFactor = UIScreen.main.scale + metalView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + addSubview(metalView) + commandQueue = device?.makeCommandQueue() + #endif + } + + func initializeRenderPipelineState() { + guard let device = device, let library = device.makeDefaultLibrary() else { + return + } + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.sampleCount = 1 + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + pipelineDescriptor.depthAttachmentPixelFormat = .invalid + + pipelineDescriptor.vertexFunction = library.makeFunction(name: "mapTexture") + pipelineDescriptor.fragmentFunction = library.makeFunction(name: "displayNV12Texture") + + renderPipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) + } + + func initializeTextureCache() { + #if os(iOS) && (!arch(i386) && !arch(x86_64)) + guard let metalDevice = metalDevice, + CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, metalDevice, nil, &textureCache) == kCVReturnSuccess else { + return + } + #endif + } + +#if os(iOS) && (!arch(i386) && !arch(x86_64)) + func texture(pixelBuffer: CVPixelBuffer, textureCache: CVMetalTextureCache?, planeIndex: Int = 0, pixelFormat: MTLPixelFormat = .bgra8Unorm) -> MTLTexture? { + guard let textureCache = textureCache, CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) == kCVReturnSuccess else { + return nil + } + defer { + CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) + } + + let isPlanar = CVPixelBufferIsPlanar(pixelBuffer) + let width = isPlanar ? CVPixelBufferGetWidthOfPlane(pixelBuffer, planeIndex) : CVPixelBufferGetWidth(pixelBuffer) + let height = isPlanar ? CVPixelBufferGetHeightOfPlane(pixelBuffer, planeIndex) : CVPixelBufferGetHeight(pixelBuffer) + + var imageTexture: CVMetalTexture? + let result = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache, pixelBuffer, nil, pixelFormat, width, height, planeIndex, &imageTexture) + + guard let unwrappedImageTexture = imageTexture, + let texture = CVMetalTextureGetTexture(unwrappedImageTexture), + result == kCVReturnSuccess + else { + return nil + } + + return texture + } +#endif +} + +#if os(iOS) && (!arch(i386) && !arch(x86_64)) +extension AgoraMetalRender: MTKViewDelegate { + public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { + + } + + public func draw(in: MTKView) { + guard viewSize.width > 0 && viewSize.height > 0 else { + return + } + + _ = semaphore.wait(timeout: .distantFuture) + guard let textures = textures, let device = device, + let commandBuffer = commandQueue?.makeCommandBuffer(), let vertexBuffer = vertexBuffer else { + semaphore.signal() + return + } + + render(textures: textures, withCommandBuffer: commandBuffer, device: device, vertexBuffer: vertexBuffer) + } + + private func render(textures: [MTLTexture], withCommandBuffer commandBuffer: MTLCommandBuffer, device: MTLDevice, vertexBuffer: MTLBuffer) { + guard let currentRenderPassDescriptor = metalView.currentRenderPassDescriptor, + let currentDrawable = metalView.currentDrawable, + let renderPipelineState = renderPipelineState, + let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: currentRenderPassDescriptor) else { + semaphore.signal() + return + } + + encoder.pushDebugGroup("Agora-Custom-Render-Frame") + encoder.setRenderPipelineState(renderPipelineState) + encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + if let textureY = textures.first, let textureUV = textures.last { + encoder.setFragmentTexture(textureY, index: 0) + encoder.setFragmentTexture(textureUV, index: 1) + encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) + } + + encoder.popDebugGroup() + encoder.endEncoding() + + commandBuffer.addScheduledHandler { [weak self] (buffer) in + self?.semaphore.signal() + } + commandBuffer.present(currentDrawable) + commandBuffer.commit() + } +} +#endif + +extension AgoraVideoRotation { + func renderedCoordinates(mirror: Bool, videoSize: CGSize, viewSize: CGSize) -> [float4]? { + guard viewSize.width > 0, viewSize.height > 0, videoSize.width > 0, videoSize.height > 0 else { + return nil + } + + let widthAspito: Float + let heightAspito: Float + if self == .rotation90 || self == .rotation270 { + widthAspito = Float(videoSize.height / viewSize.width) + heightAspito = Float(videoSize.width / viewSize.height) + } else { + widthAspito = Float(videoSize.width / viewSize.width) + heightAspito = Float(videoSize.height / viewSize.height) + } + + let x: Float + let y: Float + if widthAspito < heightAspito { + x = 1 + y = heightAspito / widthAspito + } else { + x = widthAspito / heightAspito + y = 1 + } + + let A = float4( x, -y, 0.0, 1.0 ) + let B = float4( -x, -y, 0.0, 1.0 ) + let C = float4( x, y, 0.0, 1.0 ) + let D = float4( -x, y, 0.0, 1.0 ) + + switch self { + case .rotationNone: + if mirror { + return [A, B, C, D] + } else { + return [B, A, D, C] + } + case .rotation90: + if mirror { + return [C, A, D, B] + } else { + return [D, B, C, A] + } + case .rotation180: + if mirror { + return [D, C, B, A] + } else { + return [C, D, A, B] + } + case .rotation270: + if mirror { + return [B, D, A, C] + } else { + return [A, C, B, D] + } + } + } +} diff --git a/iOS/APIExample/Common/ExternalVideo/AgoraMetalShader.metal b/iOS/APIExample/Common/ExternalVideo/AgoraMetalShader.metal new file mode 100644 index 000000000..ab28c9968 --- /dev/null +++ b/iOS/APIExample/Common/ExternalVideo/AgoraMetalShader.metal @@ -0,0 +1,70 @@ +// +// AgoraMetalShader.metal +// Agora-Custom-Media-Device +// +// Created by GongYuhua on 2017/11/15. +// Copyright © 2017年 Agora. All rights reserved. +// + +#include + +using namespace metal; + +typedef struct { + float4 renderedCoordinate [[position]]; + float2 textureCoordinate; +} TextureMappingVertex; + +vertex TextureMappingVertex mapTexture(unsigned int vertex_id [[ vertex_id ]], + const device packed_float4* vertex_array [[ buffer(0) ]]) { + + float4x4 renderedCoordinates = float4x4(vertex_array[0], vertex_array[1], vertex_array[2], vertex_array[3]); + float4x2 textureCoordinates = float4x2(float2( 0.0, 1.0 ), + float2( 1.0, 1.0 ), + float2( 0.0, 0.0 ), + float2( 1.0, 0.0 )); + + TextureMappingVertex outVertex; + outVertex.renderedCoordinate = renderedCoordinates[vertex_id]; + outVertex.textureCoordinate = textureCoordinates[vertex_id]; + + return outVertex; +} + +fragment float4 displayNV12Texture(TextureMappingVertex mappingVertex [[stage_in]], + texture2d textureY [[ texture(0) ]], + texture2d textureUV [[ texture(1) ]]) { + constexpr sampler colorSampler(mip_filter::linear, + mag_filter::linear, + min_filter::linear); + + const float4x4 ycbcrToRGBTransform = float4x4(float4(+1.0000f, +1.0000f, +1.0000f, +0.0000f), + float4(+0.0000f, -0.3441f, +1.7720f, +0.0000f), + float4(+1.4020f, -0.7141f, +0.0000f, +0.0000f), + float4(-0.7010f, +0.5291f, -0.8860f, +1.0000f)); + + float4 ycbcr = float4(textureY.sample(colorSampler, mappingVertex.textureCoordinate).r, + textureUV.sample(colorSampler, mappingVertex.textureCoordinate).rg, 1.0); + return ycbcrToRGBTransform * ycbcr; +} + +kernel void writeRGBFromYUV(texture2d yTexture [[texture(0)]], + texture2d uTexture [[texture(1)]], + texture2d vTexture [[texture(2)]], + texture2d rgbTexture [[texture(3)]], + uint2 yPosition [[thread_position_in_grid]]) +{ + float3x3 yuvToRGBTransform = float3x3(float3(+1.0000f, +1.0000f, +1.0000f), + float3(+0.0000f, -0.3441f, +1.7720f), + float3(+1.4020f, -0.7141f, +0.0000f)); + + uint2 uvPosition = uint2(yPosition.x / 2, yPosition.y / 2); + + float3 yuvMatrix = float3(yTexture.read(yPosition).r / 255.0, + uTexture.read(uvPosition).r / 255.0 - 0.5, + vTexture.read(uvPosition).r / 255.0 - 0.5); + + float3 rgbMatrix = yuvToRGBTransform * yuvMatrix; + + rgbTexture.write(float4(float3(rgbMatrix), 1.0), yPosition); +} diff --git a/iOS/APIExample/Common/GlobalSettings.swift b/iOS/APIExample/Common/GlobalSettings.swift new file mode 100644 index 000000000..0cf8011da --- /dev/null +++ b/iOS/APIExample/Common/GlobalSettings.swift @@ -0,0 +1,73 @@ +// +// GlobalSettings.swift +// APIExample +// +// Created by 张乾泽 on 2020/9/25. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import Foundation + +let SCREEN_SHARE_UID_MIN:UInt = 501 +let SCREEN_SHARE_UID_MAX:UInt = 1000 +let SCREEN_SHARE_BROADCASTER_UID_MIN:UInt = 1001 +let SCREEN_SHARE_BROADCASTER_UID_MAX:UInt = 2000 + +let SCREEN_SHARE_UID = UInt.random(in: SCREEN_SHARE_UID_MIN...SCREEN_SHARE_UID_MAX) +let SCREEN_SHARE_BROADCASTER_UID = UInt.random(in: SCREEN_SHARE_BROADCASTER_UID_MIN...SCREEN_SHARE_BROADCASTER_UID_MAX) + +struct SettingItemOption { + var idx: Int + var label:String + var value:Any +} + +class SettingItem { + var selected: Int + var options: [SettingItemOption] + func selectedOption() -> SettingItemOption { + return options[selected] + } + + init(selected: Int, options: [SettingItemOption]) { + self.selected = selected + self.options = options + } +} + +class GlobalSettings { + // The region for connection. This advanced feature applies to scenarios that have regional restrictions. + // For the regions that Agora supports, see https://docs.agora.io/en/Interactive%20Broadcast/API%20Reference/oc/Constants/AgoraAreaCode.html. After specifying the region, the SDK connects to the Agora servers within that region. + var area:AgoraAreaCode = .GLOB + static let shared = GlobalSettings() + var settings:[String:SettingItem] = [ + "resolution": SettingItem(selected: 3, options: [ + SettingItemOption(idx: 0, label: "90x90", value: CGSize(width: 90, height: 90)), + SettingItemOption(idx: 1, label: "160x120", value: CGSize(width: 160, height: 120)), + SettingItemOption(idx: 2, label: "320x240", value: CGSize(width: 320, height: 240)), + SettingItemOption(idx: 3, label: "640x360", value: CGSize(width: 640, height: 360)), + SettingItemOption(idx: 4, label: "1280x720", value: CGSize(width: 1280, height: 720)) + ]), + "fps": SettingItem(selected: 3, options: [ + SettingItemOption(idx: 0, label: "10fps", value: AgoraVideoFrameRate.fps10), + SettingItemOption(idx: 1, label: "15fps", value: AgoraVideoFrameRate.fps15), + SettingItemOption(idx: 2, label: "24fps", value: AgoraVideoFrameRate.fps24), + SettingItemOption(idx: 3, label: "30fps", value: AgoraVideoFrameRate.fps30), + SettingItemOption(idx: 4, label: "60fps", value: AgoraVideoFrameRate.fps60) + ]), + "orientation": SettingItem(selected: 0, options: [ + SettingItemOption(idx: 0, label: "adaptive".localized, value: AgoraVideoOutputOrientationMode.adaptative), + SettingItemOption(idx: 1, label: "fixed portrait".localized, value: AgoraVideoOutputOrientationMode.fixedPortrait), + SettingItemOption(idx: 2, label: "fixed landscape".localized, value: AgoraVideoOutputOrientationMode.fixedLandscape) + ]), + "area": SettingItem(selected: 0, options: [ + SettingItemOption(idx: 0, label: "adaptive".localized, value: AgoraAreaCode.GLOB), + SettingItemOption(idx: 1, label: "fixed portrait".localized, value: AgoraVideoOutputOrientationMode.fixedPortrait), + SettingItemOption(idx: 2, label: "fixed landscape".localized, value: AgoraVideoOutputOrientationMode.fixedLandscape) + ]) + ] + + func getSetting(key:String) -> SettingItem? { + return settings[key] + } +} diff --git a/iOS/APIExample/Common/LogViewController.swift b/iOS/APIExample/Common/LogViewController.swift index 07bf97e08..ae52c2ecd 100644 --- a/iOS/APIExample/Common/LogViewController.swift +++ b/iOS/APIExample/Common/LogViewController.swift @@ -6,8 +6,8 @@ // Copyright © 2020 Agora Corp. All rights reserved. // -import Foundation import UIKit +import Foundation enum LogLevel { case info, warning, error @@ -22,25 +22,55 @@ enum LogLevel { } struct LogItem { - var msg:String + var message:String var level:LogLevel var dateTime:Date } class LogUtils { static var logs:[LogItem] = [] + static var appLogPath:String = "\(logFolder())/app-\(Date().getFormattedDate(format: "yyyy-MM-dd")).log" - static func log(msg:String, level: LogLevel) { - LogUtils.logs.append(LogItem(msg: msg, level: level, dateTime: Date())) - print("\(level.description): \(msg)") + static func log(message: String, level: LogLevel) { + LogUtils.logs.append(LogItem(message: message, level: level, dateTime: Date())) + print("\(level.description): \(message)") + } + + static func logFolder() -> String { + let folder = "\(NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])/logs" + try? FileManager.default.createDirectory(atPath: folder, withIntermediateDirectories: true, attributes: nil) + return folder + } + static func sdkLogPath() -> String { + let logPath = "\(logFolder())/agorasdk.log" + return logPath } static func removeAll() { LogUtils.logs.removeAll() } + + static func writeAppLogsToDisk() { + if let outputStream = OutputStream(url: URL(fileURLWithPath: LogUtils.appLogPath), append: true) { + outputStream.open() + for log in LogUtils.logs { + let msg = "\(log.level.description) \(log.dateTime.getFormattedDate(format: "yyyy-MM-dd HH:mm:ss")) \(log.message)\n" + let bytesWritten = outputStream.write(msg) + if bytesWritten < 0 { print("write failure") } + } + outputStream.close() + LogUtils.removeAll() + } else { + print("Unable to open file") + } + } + + static func cleanUp() { + try? FileManager.default.removeItem(at: URL(fileURLWithPath: LogUtils.logFolder(), isDirectory: true)) + } } -class LogViewController: UIViewController { +class LogViewController: AGViewController { } @@ -61,7 +91,7 @@ extension LogViewController: UITableViewDataSource { cell?.textLabel?.lineBreakMode = .byWordWrapping; let dateFormatterPrint = DateFormatter() dateFormatterPrint.dateFormat = "yyyy-MM-dd HH:mm:ss" - cell?.textLabel?.text = "\(dateFormatterPrint.string(from: logitem.dateTime)) - \(logitem.level.description): \(logitem.msg)" + cell?.textLabel?.text = "\(dateFormatterPrint.string(from: logitem.dateTime)) - \(logitem.level.description): \(logitem.message)" return cell! } } diff --git a/iOS/APIExample/Common/RawDataApi/AgoraMediaDataPlugin.h b/iOS/APIExample/Common/RawDataApi/AgoraMediaDataPlugin.h new file mode 100644 index 000000000..3d25e55ea --- /dev/null +++ b/iOS/APIExample/Common/RawDataApi/AgoraMediaDataPlugin.h @@ -0,0 +1,88 @@ +// +// AgoraMediaDataPlugin.h +// OpenVideoCall +// +// Created by CavanSu on 26/02/2018. +// Copyright © 2018 Agora. All rights reserved. +// + +#import "AgoraMediaRawData.h" + +#if (!(TARGET_OS_IPHONE) && (TARGET_OS_MAC)) +#import +typedef NSImage AGImage; +#else +#import +typedef UIImage AGImage; +#endif + +typedef NS_OPTIONS(NSInteger, ObserverVideoType) { + ObserverVideoTypeCaptureVideo = 1 << 0, + ObserverVideoTypeRenderVideo = 1 << 1, + ObserverVideoTypePreEncodeVideo = 1 << 2 +}; + +typedef NS_OPTIONS(NSInteger, ObserverAudioType) { + ObserverAudioTypeRecordAudio = 1 << 0, + ObserverAudioTypePlaybackAudio = 1 << 1, + ObserverAudioTypePlaybackAudioFrameBeforeMixing = 1 << 2, + ObserverAudioTypeMixedAudio = 1 << 3 +}; + +typedef NS_OPTIONS(NSInteger, ObserverPacketType) { + ObserverPacketTypeSendAudio = 1 << 0, + ObserverPacketTypeSendVideo = 1 << 1, + ObserverPacketTypeReceiveAudio = 1 << 2, + ObserverPacketTypeReceiveVideo = 1 << 3 +}; + +@class AgoraRtcEngineKit; +@class AgoraMediaDataPlugin; +@protocol AgoraVideoDataPluginDelegate +@optional +- (AgoraVideoRawData * _Nonnull)mediaDataPlugin:(AgoraMediaDataPlugin * _Nonnull)mediaDataPlugin didCapturedVideoRawData:(AgoraVideoRawData * _Nonnull)videoRawData; +- (AgoraVideoRawData * _Nonnull)mediaDataPlugin:(AgoraMediaDataPlugin * _Nonnull)mediaDataPlugin willRenderVideoRawData:(AgoraVideoRawData * _Nonnull)videoRawData ofUid:(uint)uid; +- (AgoraVideoRawData * _Nonnull)mediaDataPlugin:(AgoraMediaDataPlugin * _Nonnull)mediaDataPlugin willPreEncodeVideoRawData:(AgoraVideoRawData * _Nonnull)videoRawData; +@end + +@protocol AgoraAudioDataPluginDelegate +@optional +- (AgoraAudioRawData * _Nonnull)mediaDataPlugin:(AgoraMediaDataPlugin * _Nonnull)mediaDataPlugin didRecordAudioRawData:(AgoraAudioRawData * _Nonnull)audioRawData; +- (AgoraAudioRawData * _Nonnull)mediaDataPlugin:(AgoraMediaDataPlugin * _Nonnull)mediaDataPlugin willPlaybackAudioRawData:(AgoraAudioRawData * _Nonnull)audioRawData; +- (AgoraAudioRawData * _Nonnull)mediaDataPlugin:(AgoraMediaDataPlugin * _Nonnull)mediaDataPlugin willPlaybackBeforeMixingAudioRawData:(AgoraAudioRawData * _Nonnull)audioRawData ofUid:(uint)uid; +- (AgoraAudioRawData * _Nonnull)mediaDataPlugin:(AgoraMediaDataPlugin * _Nonnull)mediaDataPlugin didMixedAudioRawData:(AgoraAudioRawData * _Nonnull)audioRawData; +@end + +@protocol AgoraPacketDataPluginDelegate +@optional +- (AgoraPacketRawData * _Nonnull)mediaDataPlugin:(AgoraMediaDataPlugin * _Nonnull)mediaDataPlugin willSendAudioPacket:(AgoraPacketRawData * _Nonnull)audioPacket; +- (AgoraPacketRawData * _Nonnull)mediaDataPlugin:(AgoraMediaDataPlugin * _Nonnull)mediaDataPlugin willSendVideoPacket:(AgoraPacketRawData * _Nonnull)videoPacket; + +- (AgoraPacketRawData * _Nonnull)mediaDataPlugin:(AgoraMediaDataPlugin * _Nonnull)mediaDataPlugin didReceivedAudioPacket:(AgoraPacketRawData * _Nonnull)audioPacket; +- (AgoraPacketRawData * _Nonnull)mediaDataPlugin:(AgoraMediaDataPlugin * _Nonnull)mediaDataPlugin didReceivedVideoPacket:(AgoraPacketRawData * _Nonnull)videoPacket; +@end + +@interface AgoraMediaDataPlugin : NSObject +@property (nonatomic, weak) id _Nullable videoDelegate; +@property (nonatomic, weak) id _Nullable audioDelegate; +@property (nonatomic, weak) id _Nullable packetDelegate; + ++ (instancetype _Nonnull)mediaDataPluginWithAgoraKit:(AgoraRtcEngineKit * _Nonnull)agoraKit; + +- (void)registerVideoRawDataObserver:(ObserverVideoType)observerType; +- (void)deregisterVideoRawDataObserver:(ObserverVideoType)observerType; + +- (void)registerAudioRawDataObserver:(ObserverAudioType)observerType; +- (void)deregisterAudioRawDataObserver:(ObserverAudioType)observerType; + +- (void)registerPacketRawDataObserver:(ObserverPacketType)observerType; +- (void)deregisterPacketRawDataObserver:(ObserverPacketType)observerType; + +- (void)setVideoRawDataFormatter:(AgoraVideoRawDataFormatter * _Nonnull)formatter; +- (AgoraVideoRawDataFormatter * _Nonnull)getCurrentVideoRawDataFormatter; + +// you can call following methods before set videoDelegate +- (void)localSnapshot:(void (^ _Nullable)(AGImage * _Nonnull image))completion; +- (void)remoteSnapshotWithUid:(NSUInteger)uid image:(void (^ _Nullable)(AGImage * _Nonnull image))completion; +@end + diff --git a/iOS/APIExample/Common/RawDataApi/AgoraMediaDataPlugin.mm b/iOS/APIExample/Common/RawDataApi/AgoraMediaDataPlugin.mm new file mode 100644 index 000000000..a0e536618 --- /dev/null +++ b/iOS/APIExample/Common/RawDataApi/AgoraMediaDataPlugin.mm @@ -0,0 +1,515 @@ +// +// AgoraMediaRawData.m +// OpenVideoCall +// +// Created by CavanSu on 26/02/2018. +// Copyright © 2018 Agora. All rights reserved. +// + +#import "AgoraMediaDataPlugin.h" + +#import +#import +#import +#include + +typedef void (^imageBlock)(AGImage *image); + +@interface AgoraMediaDataPlugin () +@property (nonatomic, assign) NSUInteger screenShotUid; +@property (nonatomic, assign) ObserverVideoType observerVideoType; +@property (nonatomic, assign) ObserverAudioType observerAudioType; +@property (nonatomic, assign) ObserverPacketType observerPacketType; +@property (nonatomic, strong) AgoraVideoRawDataFormatter *videoFormatter; +@property (nonatomic, weak) AgoraRtcEngineKit *agoraKit; +@property (nonatomic, copy) imageBlock imageBlock; +- (void)yuvToUIImageWithVideoRawData:(AgoraVideoRawData *)data; +@end + + +class AgoraMediaDataPluginVideoFrameObserver : public agora::media::IVideoFrameObserver +{ +public: + AgoraMediaDataPlugin *mediaDataPlugin; + BOOL getOneDidCaptureVideoFrame = false; + BOOL getOneWillRenderVideoFrame = false; + unsigned int videoFrameUid = -1; + + AgoraVideoRawData* getVideoRawDataWithVideoFrame(VideoFrame& videoFrame) + { + AgoraVideoRawData *data = [[AgoraVideoRawData alloc] init]; + data.type = videoFrame.type; + data.width = videoFrame.width; + data.height = videoFrame.height; + data.yStride = videoFrame.yStride; + data.uStride = videoFrame.uStride; + data.vStride = videoFrame.vStride; + data.rotation = videoFrame.rotation; + data.renderTimeMs = videoFrame.renderTimeMs; + data.yBuffer = (char *)videoFrame.yBuffer; + data.uBuffer = (char *)videoFrame.uBuffer; + data.vBuffer = (char *)videoFrame.vBuffer; + return data; + } + + void modifiedVideoFrameWithNewVideoRawData(VideoFrame& videoFrame, AgoraVideoRawData *videoRawData) + { + videoFrame.width = videoRawData.width; + videoFrame.height = videoRawData.height; + videoFrame.yStride = videoRawData.yStride; + videoFrame.uStride = videoRawData.uStride; + videoFrame.vStride = videoRawData.vStride; + videoFrame.rotation = videoRawData.rotation; + videoFrame.renderTimeMs = videoRawData.renderTimeMs; + } + + virtual bool onCaptureVideoFrame(VideoFrame& videoFrame) override + { + if (!mediaDataPlugin && ((mediaDataPlugin.observerVideoType >> 0) == 0)) return true; + @autoreleasepool { + AgoraVideoRawData *newData = nil; + if ([mediaDataPlugin.videoDelegate respondsToSelector:@selector(mediaDataPlugin:didCapturedVideoRawData:)]) { + AgoraVideoRawData *data = getVideoRawDataWithVideoFrame(videoFrame); + newData = [mediaDataPlugin.videoDelegate mediaDataPlugin:mediaDataPlugin didCapturedVideoRawData:data]; + modifiedVideoFrameWithNewVideoRawData(videoFrame, newData); + + // ScreenShot + if (getOneDidCaptureVideoFrame) { + getOneDidCaptureVideoFrame = false; + [mediaDataPlugin yuvToUIImageWithVideoRawData:newData]; + } + } + } + return true; + } + + virtual bool onRenderVideoFrame(unsigned int uid, VideoFrame& videoFrame) override + { + if (!mediaDataPlugin && ((mediaDataPlugin.observerVideoType >> 1) == 0)) return true; + @autoreleasepool { + AgoraVideoRawData *newData = nil; + if ([mediaDataPlugin.videoDelegate respondsToSelector:@selector(mediaDataPlugin:willRenderVideoRawData:ofUid:)]) { + AgoraVideoRawData *data = getVideoRawDataWithVideoFrame(videoFrame); + newData = [mediaDataPlugin.videoDelegate mediaDataPlugin:mediaDataPlugin willRenderVideoRawData:data ofUid:uid]; + modifiedVideoFrameWithNewVideoRawData(videoFrame, newData); + + // ScreenShot + if (getOneWillRenderVideoFrame && videoFrameUid == uid) { + getOneWillRenderVideoFrame = false; + videoFrameUid = -1; + [mediaDataPlugin yuvToUIImageWithVideoRawData:newData]; + } + } + } + return true; + } + + virtual bool onPreEncodeVideoFrame(VideoFrame& videoFrame) override + { + if (!mediaDataPlugin && ((mediaDataPlugin.observerVideoType >> 2) == 0)) return true; + @autoreleasepool { + AgoraVideoRawData *newData = nil; + if ([mediaDataPlugin.videoDelegate respondsToSelector:@selector(mediaDataPlugin:willPreEncodeVideoRawData:)]) { + AgoraVideoRawData *data = getVideoRawDataWithVideoFrame(videoFrame); + newData = [mediaDataPlugin.videoDelegate mediaDataPlugin:mediaDataPlugin willPreEncodeVideoRawData:data]; + modifiedVideoFrameWithNewVideoRawData(videoFrame, newData); + } + } + return true; + } + + virtual VIDEO_FRAME_TYPE getVideoFormatPreference() override + { + return VIDEO_FRAME_TYPE(mediaDataPlugin.videoFormatter.type); + } + + virtual bool getRotationApplied() override + { + return mediaDataPlugin.videoFormatter.rotationApplied; + } + + virtual bool getMirrorApplied() override + { + return mediaDataPlugin.videoFormatter.mirrorApplied; + } +}; + +class AgoraMediaDataPluginAudioFrameObserver : public agora::media::IAudioFrameObserver +{ +public: + AgoraMediaDataPlugin *mediaDataPlugin; + + AgoraAudioRawData* getAudioRawDataWithAudioFrame(AudioFrame& audioFrame) + { + AgoraAudioRawData *data = [[AgoraAudioRawData alloc] init]; + data.samples = audioFrame.samples; + data.bytesPerSample = audioFrame.bytesPerSample; + data.channels = audioFrame.channels; + data.samplesPerSec = audioFrame.samplesPerSec; + data.renderTimeMs = audioFrame.renderTimeMs; + data.buffer = (char *)audioFrame.buffer; + data.bufferSize = audioFrame.samples * audioFrame.bytesPerSample; + return data; + } + + void modifiedAudioFrameWithNewAudioRawData(AudioFrame& audioFrame, AgoraAudioRawData *audioRawData) + { + audioFrame.samples = audioRawData.samples; + audioFrame.bytesPerSample = audioRawData.bytesPerSample; + audioFrame.channels = audioRawData.channels; + audioFrame.samplesPerSec = audioRawData.samplesPerSec; + audioFrame.renderTimeMs = audioRawData.renderTimeMs; + } + + virtual bool onRecordAudioFrame(AudioFrame& audioFrame) override + { + if (!mediaDataPlugin && ((mediaDataPlugin.observerAudioType >> 0) == 0)) return true; + @autoreleasepool { + if ([mediaDataPlugin.audioDelegate respondsToSelector:@selector(mediaDataPlugin:didRecordAudioRawData:)]) { + AgoraAudioRawData *data = getAudioRawDataWithAudioFrame(audioFrame); + AgoraAudioRawData *newData = [mediaDataPlugin.audioDelegate mediaDataPlugin:mediaDataPlugin didRecordAudioRawData:data]; + modifiedAudioFrameWithNewAudioRawData(audioFrame, newData); + } + } + return true; + } + + virtual bool onPlaybackAudioFrame(AudioFrame& audioFrame) override + { + if (!mediaDataPlugin && ((mediaDataPlugin.observerAudioType >> 1) == 0)) return true; + @autoreleasepool { + if ([mediaDataPlugin.audioDelegate respondsToSelector:@selector(mediaDataPlugin:willPlaybackAudioRawData:)]) { + AgoraAudioRawData *data = getAudioRawDataWithAudioFrame(audioFrame); + AgoraAudioRawData *newData = [mediaDataPlugin.audioDelegate mediaDataPlugin:mediaDataPlugin willPlaybackAudioRawData:data]; + modifiedAudioFrameWithNewAudioRawData(audioFrame, newData); + } + } + return true; + } + + virtual bool onPlaybackAudioFrameBeforeMixing(unsigned int uid, AudioFrame& audioFrame) override + { + if (!mediaDataPlugin && ((mediaDataPlugin.observerAudioType >> 2) == 0)) return true; + @autoreleasepool { + if ([mediaDataPlugin.audioDelegate respondsToSelector:@selector(mediaDataPlugin:willPlaybackBeforeMixingAudioRawData:ofUid:)]) { + AgoraAudioRawData *data = getAudioRawDataWithAudioFrame(audioFrame); + AgoraAudioRawData *newData = [mediaDataPlugin.audioDelegate mediaDataPlugin:mediaDataPlugin willPlaybackBeforeMixingAudioRawData:data ofUid:uid]; + modifiedAudioFrameWithNewAudioRawData(audioFrame, newData); + } + } + return true; + } + + virtual bool onMixedAudioFrame(AudioFrame& audioFrame) override + { + if (!mediaDataPlugin && ((mediaDataPlugin.observerAudioType >> 3) == 0)) return true; + @autoreleasepool { + if ([mediaDataPlugin.audioDelegate respondsToSelector:@selector(mediaDataPlugin:didMixedAudioRawData:)]) { + AgoraAudioRawData *data = getAudioRawDataWithAudioFrame(audioFrame); + AgoraAudioRawData *newData = [mediaDataPlugin.audioDelegate mediaDataPlugin:mediaDataPlugin didMixedAudioRawData:data]; + modifiedAudioFrameWithNewAudioRawData(audioFrame, newData); + } + } + return true; + } +}; + +class AgoraMediaDataPluginPacketObserver : public agora::rtc::IPacketObserver +{ +public: + AgoraMediaDataPlugin *mediaDataPlugin; + + AgoraMediaDataPluginPacketObserver() + { + } + + AgoraPacketRawData* getPacketRawDataWithPacket(Packet& packet) + { + AgoraPacketRawData *data = [[AgoraPacketRawData alloc] init]; + data.buffer = packet.buffer; + data.bufferSize = packet.size; + return data; + } + + void modifiedPacketWithNewPacketRawData(Packet& packet, AgoraPacketRawData *rawData) + { + packet.size = rawData.bufferSize; + } + + virtual bool onSendAudioPacket(Packet& packet) + { + if (!mediaDataPlugin && ((mediaDataPlugin.observerPacketType >> 0) == 0)) return true; + @autoreleasepool { + if ([mediaDataPlugin.packetDelegate respondsToSelector:@selector(mediaDataPlugin:willSendAudioPacket:)]) { + AgoraPacketRawData *data = getPacketRawDataWithPacket(packet); + AgoraPacketRawData *newData = [mediaDataPlugin.packetDelegate mediaDataPlugin:mediaDataPlugin willSendAudioPacket:data]; + modifiedPacketWithNewPacketRawData(packet, newData); + } + } + return true; + } + + virtual bool onSendVideoPacket(Packet& packet) + { + if (!mediaDataPlugin && ((mediaDataPlugin.observerPacketType >> 1) == 0)) return true; + @autoreleasepool { + if ([mediaDataPlugin.packetDelegate respondsToSelector:@selector(mediaDataPlugin:willSendVideoPacket:)]) { + AgoraPacketRawData *data = getPacketRawDataWithPacket(packet); + AgoraPacketRawData *newData = [mediaDataPlugin.packetDelegate mediaDataPlugin:mediaDataPlugin willSendVideoPacket:data]; + modifiedPacketWithNewPacketRawData(packet, newData); + } + } + return true; + } + + virtual bool onReceiveAudioPacket(Packet& packet) + { + if (!mediaDataPlugin && ((mediaDataPlugin.observerPacketType >> 2) == 0)) return true; + @autoreleasepool { + if ([mediaDataPlugin.packetDelegate respondsToSelector:@selector(mediaDataPlugin:didReceivedAudioPacket:)]) { + AgoraPacketRawData *data = getPacketRawDataWithPacket(packet); + AgoraPacketRawData *newData = [mediaDataPlugin.packetDelegate mediaDataPlugin:mediaDataPlugin didReceivedAudioPacket:data]; + modifiedPacketWithNewPacketRawData(packet, newData); + } + } + return true; + } + + virtual bool onReceiveVideoPacket(Packet& packet) + { + if (!mediaDataPlugin && ((mediaDataPlugin.observerPacketType >> 3) == 0)) return true; + @autoreleasepool { + if ([mediaDataPlugin.packetDelegate respondsToSelector:@selector(mediaDataPlugin:didReceivedVideoPacket:)]) { + AgoraPacketRawData *data = getPacketRawDataWithPacket(packet); + AgoraPacketRawData *newData = [mediaDataPlugin.packetDelegate mediaDataPlugin:mediaDataPlugin didReceivedVideoPacket:data]; + modifiedPacketWithNewPacketRawData(packet, newData); + } + } + return true; + } +}; + +static AgoraMediaDataPluginVideoFrameObserver s_videoFrameObserver; +static AgoraMediaDataPluginAudioFrameObserver s_audioFrameObserver; +static AgoraMediaDataPluginPacketObserver s_packetObserver; + +@implementation AgoraMediaDataPlugin + ++ (instancetype)mediaDataPluginWithAgoraKit:(AgoraRtcEngineKit *)agoraKit { + AgoraMediaDataPlugin *source = [[AgoraMediaDataPlugin alloc] init]; + source.videoFormatter = [[AgoraVideoRawDataFormatter alloc] init]; + source.agoraKit = agoraKit; + + if (!agoraKit) { + return nil; + } + return source; +} + +- (void)registerVideoRawDataObserver:(ObserverVideoType)observerType { + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)self.agoraKit.getNativeHandle; + agora::util::AutoPtr mediaEngine; + mediaEngine.queryInterface(rtc_engine, agora::AGORA_IID_MEDIA_ENGINE); + + NSInteger oldValue = self.observerVideoType; + self.observerVideoType |= observerType; + + if (mediaEngine && oldValue == 0) + { + mediaEngine->registerVideoFrameObserver(&s_videoFrameObserver); + s_videoFrameObserver.mediaDataPlugin = self; + } +} + +- (void)deregisterVideoRawDataObserver:(ObserverVideoType)observerType { + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)self.agoraKit.getNativeHandle; + agora::util::AutoPtr mediaEngine; + mediaEngine.queryInterface(rtc_engine, agora::AGORA_IID_MEDIA_ENGINE); + + self.observerVideoType ^= observerType; + + if (mediaEngine && self.observerVideoType == 0) + { + mediaEngine->registerVideoFrameObserver(NULL); + s_videoFrameObserver.mediaDataPlugin = nil; + } +} + +- (void)registerAudioRawDataObserver:(ObserverAudioType)observerType { + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)self.agoraKit.getNativeHandle; + agora::util::AutoPtr mediaEngine; + mediaEngine.queryInterface(rtc_engine, agora::AGORA_IID_MEDIA_ENGINE); + + NSInteger oldValue = self.observerAudioType; + self.observerAudioType |= observerType; + + if (mediaEngine && oldValue == 0) + { + mediaEngine->registerAudioFrameObserver(&s_audioFrameObserver); + s_audioFrameObserver.mediaDataPlugin = self; + } +} + +- (void)deregisterAudioRawDataObserver:(ObserverAudioType)observerType { + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)self.agoraKit.getNativeHandle; + agora::util::AutoPtr mediaEngine; + mediaEngine.queryInterface(rtc_engine, agora::AGORA_IID_MEDIA_ENGINE); + + self.observerAudioType ^= observerType; + + if (mediaEngine && self.observerAudioType == 0) + { + mediaEngine->registerAudioFrameObserver(NULL); + s_audioFrameObserver.mediaDataPlugin = nil; + } +} + +- (void)registerPacketRawDataObserver:(ObserverPacketType)observerType { + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)self.agoraKit.getNativeHandle; + + NSInteger oldValue = self.observerPacketType; + self.observerPacketType |= observerType; + + if (rtc_engine && oldValue == 0) + { + rtc_engine->registerPacketObserver(&s_packetObserver); + s_packetObserver.mediaDataPlugin = self; + } +} + +- (void)deregisterPacketRawDataObserver:(ObserverPacketType)observerType { + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)self.agoraKit.getNativeHandle; + + self.observerPacketType ^= observerType; + + if (rtc_engine && self.observerPacketType == 0) + { + rtc_engine->registerPacketObserver(NULL); + s_packetObserver.mediaDataPlugin = nil; + } +} + +- (void)setVideoRawDataFormatter:(AgoraVideoRawDataFormatter * _Nonnull)formatter { + if (self.videoFormatter.type != formatter.type) { + self.videoFormatter.type = formatter.type; + } + + if (self.videoFormatter.rotationApplied != formatter.rotationApplied) { + self.videoFormatter.rotationApplied = formatter.rotationApplied; + } + + if (self.videoFormatter.mirrorApplied != formatter.mirrorApplied) { + self.videoFormatter.mirrorApplied = formatter.mirrorApplied; + } +} + +- (AgoraVideoRawDataFormatter * _Nonnull)getCurrentVideoRawDataFormatter { + return self.videoFormatter; +} + +#pragma mark - Screen Capture +- (void)localSnapshot:(void (^ _Nullable)(AGImage * _Nonnull image))completion { + self.imageBlock = completion; + s_videoFrameObserver.getOneDidCaptureVideoFrame = true; +} + +- (void)remoteSnapshotWithUid:(NSUInteger)uid image:(void (^ _Nullable)(AGImage * _Nonnull image))completion { + self.imageBlock = completion; + s_videoFrameObserver.getOneWillRenderVideoFrame = true; + s_videoFrameObserver.videoFrameUid = (unsigned int)uid; +} + +- (void)yuvToUIImageWithVideoRawData:(AgoraVideoRawData *)data { + size_t width = data.width; + size_t height = data.height; + size_t yStride = data.yStride; + size_t uvStride = data.uStride; + + char* yBuffer = data.yBuffer; + char* uBuffer = data.uBuffer; + char* vBuffer = data.vBuffer; + + size_t uvBufferLength = height * uvStride; + char* uvBuffer = (char *)malloc(uvBufferLength); + for (size_t uv = 0, u = 0; uv < uvBufferLength; uv += 2, u++) { + // swtich the location of U、V,to NV12 + uvBuffer[uv] = uBuffer[u]; + uvBuffer[uv+1] = vBuffer[u]; + } + + @autoreleasepool { + void * planeBaseAddress[2] = {yBuffer, uvBuffer}; + size_t planeWidth[2] = {width, width / 2}; + size_t planeHeight[2] = {height, height / 2}; + size_t planeBytesPerRow[2] = {yStride, uvStride * 2}; + + CVPixelBufferRef pixelBuffer = NULL; + CVReturn result = CVPixelBufferCreateWithPlanarBytes(kCFAllocatorDefault, + width, height, + kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + NULL, 0, + 2, planeBaseAddress, planeWidth, planeHeight, planeBytesPerRow, + NULL, NULL, NULL, + &pixelBuffer); + if (result != kCVReturnSuccess) { + NSLog(@"Unable to create cvpixelbuffer %d", result); + } + + AGImage *image = [self CVPixelBufferToImage:pixelBuffer rotation:data.rotation]; + if (self.imageBlock) { + self.imageBlock(image); + } + + CVPixelBufferRelease(pixelBuffer); + } + + if(uvBuffer != NULL) { + free(uvBuffer); + uvBuffer = NULL; + } +} + +// CVPixelBuffer-->CIImage--->AGImage Conversion +- (AGImage *)CVPixelBufferToImage:(CVPixelBufferRef)pixelBuffer rotation:(int)rotation { + size_t width, height; + CGImagePropertyOrientation orientation; + switch (rotation) { + case 0: + width = CVPixelBufferGetWidth(pixelBuffer); + height = CVPixelBufferGetHeight(pixelBuffer); + orientation = kCGImagePropertyOrientationUp; + break; + case 90: + width = CVPixelBufferGetHeight(pixelBuffer); + height = CVPixelBufferGetWidth(pixelBuffer); + orientation = kCGImagePropertyOrientationRight; + break; + case 180: + width = CVPixelBufferGetWidth(pixelBuffer); + height = CVPixelBufferGetHeight(pixelBuffer); + orientation = kCGImagePropertyOrientationDown; + break; + case 270: + width = CVPixelBufferGetHeight(pixelBuffer); + height = CVPixelBufferGetWidth(pixelBuffer); + orientation = kCGImagePropertyOrientationLeft; + break; + default: + return nil; + } + CIImage *coreImage = [[CIImage imageWithCVPixelBuffer:pixelBuffer] imageByApplyingOrientation:orientation]; + CIContext *temporaryContext = [CIContext contextWithOptions:nil]; + CGImageRef videoImage = [temporaryContext createCGImage:coreImage + fromRect:CGRectMake(0, 0, width, height)]; + +#if (!(TARGET_OS_IPHONE) && (TARGET_OS_MAC)) + AGImage *finalImage = [[NSImage alloc] initWithCGImage:videoImage size:NSMakeSize(width, height)]; +#else + AGImage *finalImage = [[AGImage alloc] initWithCGImage:videoImage]; +#endif + CGImageRelease(videoImage); + return finalImage; +} + +@end + diff --git a/iOS/APIExample/Common/RawDataApi/AgoraMediaRawData.h b/iOS/APIExample/Common/RawDataApi/AgoraMediaRawData.h new file mode 100644 index 000000000..a60375557 --- /dev/null +++ b/iOS/APIExample/Common/RawDataApi/AgoraMediaRawData.h @@ -0,0 +1,44 @@ +// +// AgoraVideoRawData.h +// OpenVideoCall +// +// Created by CavanSu on 26/02/2018. +// Copyright © 2018 Agora. All rights reserved. +// + +#import + +@interface AgoraVideoRawDataFormatter : NSObject +@property (nonatomic, assign) int type; //YUV 420, YUV 422P, RGBA +@property (nonatomic, assign) BOOL rotationApplied; +@property (nonatomic, assign) BOOL mirrorApplied; +@end + +@interface AgoraVideoRawData : NSObject +@property (nonatomic, assign) int type; +@property (nonatomic, assign) int width; //width of video frame +@property (nonatomic, assign) int height; //height of video frame +@property (nonatomic, assign) int yStride; //stride of Y data buffer +@property (nonatomic, assign) int uStride; //stride of U data buffer +@property (nonatomic, assign) int vStride; //stride of V data buffer +@property (nonatomic, assign) int rotation; // rotation of this frame (0, 90, 180, 270) +@property (nonatomic, assign) int64_t renderTimeMs; // timestamp +@property (nonatomic, assign) char* yBuffer; //Y data buffer +@property (nonatomic, assign) char* uBuffer; //U data buffer +@property (nonatomic, assign) char* vBuffer; //V data buffer +@end + +@interface AgoraAudioRawData : NSObject +@property (nonatomic, assign) int samples; //number of samples in this frame +@property (nonatomic, assign) int bytesPerSample; //number of bytes per sample: 2 for PCM16 +@property (nonatomic, assign) int channels; //number of channels (data are interleaved if stereo) +@property (nonatomic, assign) int samplesPerSec; //sampling rate +@property (nonatomic, assign) int bufferSize; +@property (nonatomic, assign) int64_t renderTimeMs; +@property (nonatomic, assign) char* buffer; //data buffer +@end + +@interface AgoraPacketRawData : NSObject +@property (nonatomic, assign) const unsigned char* buffer; +@property (nonatomic, assign) uint bufferSize; +@end diff --git a/iOS/APIExample/Common/RawDataApi/AgoraMediaRawData.m b/iOS/APIExample/Common/RawDataApi/AgoraMediaRawData.m new file mode 100644 index 000000000..7d43ddbfb --- /dev/null +++ b/iOS/APIExample/Common/RawDataApi/AgoraMediaRawData.m @@ -0,0 +1,32 @@ +// +// AgoraVideoRawData.m +// OpenVideoCall +// +// Created by CavanSu on 26/02/2018. +// Copyright © 2018 Agora. All rights reserved. +// + +#import "AgoraMediaRawData.h" + +@implementation AgoraVideoRawDataFormatter +- (instancetype)init { + if (self = [super init]) { + self.mirrorApplied = false; + self.rotationApplied = false; + self.type = 0; + } + return self; +} +@end + +@implementation AgoraVideoRawData + +@end + +@implementation AgoraAudioRawData + +@end + +@implementation AgoraPacketRawData + +@end diff --git a/iOS/APIExample/Common/RtcChannelPublishPlugin/AgoraMediaPlayerEx.cpp b/iOS/APIExample/Common/RtcChannelPublishPlugin/AgoraMediaPlayerEx.cpp new file mode 100644 index 000000000..a27336624 --- /dev/null +++ b/iOS/APIExample/Common/RtcChannelPublishPlugin/AgoraMediaPlayerEx.cpp @@ -0,0 +1,126 @@ +// +// AgoraMediaPlayerEx.cpp +// player_demo_apple +// +// Created by zhanxiaochao on 2020/5/26. +// Copyright © 2020 agora. All rights reserved. +// +#ifdef MEDIAPLAYER +#include "AgoraMediaPlayerEx.h" +#include "AudioFrameObserver.h" +#include +#include +using namespace agora::media::base; +using namespace agora::rtc; +using namespace std; +class AgoraMediaPlayerEx : public IAgoraMediaPlayerEx,public agora::media::base::IVideoFrameObserver,public agora::media::base::IAudioFrameObserver +{ +public: + ///get ms timestamp + int64_t GetHighAccuracyTickCount(){ + typedef chrono::time_point microClock_type; + microClock_type tp = chrono::time_point_cast(chrono::system_clock::now()); + return tp.time_since_epoch().count(); + } + ///push videoFrame + virtual void onFrame(const VideoFrame* frame){ + if (!is_push_video_) { + return; + } + int size = frame->width * frame->height; + uint8_t *tmp = (uint8_t *)malloc(size * 3/2); + memcpy(tmp, frame->yBuffer, size); + memcpy(tmp + size, frame->uBuffer, size >> 2); + memcpy(tmp+ size + frame->width * frame->height/4, frame->vBuffer, size >> 2); + agora::media::ExternalVideoFrame vframe; + vframe.stride = frame->yStride; + vframe.height = frame->height; + vframe.timestamp = static_cast(GetHighAccuracyTickCount()); + vframe.rotation = 0; + vframe.type = agora::media::ExternalVideoFrame::VIDEO_BUFFER_TYPE::VIDEO_BUFFER_RAW_DATA; + vframe.format = agora::media::ExternalVideoFrame::VIDEO_PIXEL_FORMAT::VIDEO_PIXEL_I420; + vframe.cropLeft = 0; + vframe.cropTop = 0; + vframe.cropBottom = 0; + vframe.cropRight = 0; + vframe.buffer = tmp; + agora::util::AutoPtr mediaEngine; + mediaEngine.queryInterface(rtcEngine_, agora::AGORA_IID_MEDIA_ENGINE); + + if (mediaEngine) + mediaEngine->pushVideoFrame(&vframe); + + free(tmp); + } + ///pushAudioFrame + virtual void onFrame(const AudioPcmFrame* frame){ + if (!is_push_audio_) { + return; + } + audioFrameObserver_->pushData((char *)&frame->data_[0], (int)(frame->samples_per_channel_ * frame->bytes_per_sample)); + } + virtual void detachPlayerFromRtc(){ + if (player_) { + player_->unregisterPlayerObserver(observer_); + player_->unregisterAudioFrameObserver(this); + player_->unregisterVideoFrameObserver(this); + observer_ = nullptr; + player_ = nullptr; + } + + + } + virtual void attachMediaPlayer(agora::rtc::IMediaPlayer *player,agora::rtc::IRtcEngine *rtcEngine) + { + audioFrameObserver_.reset(new AgoraAudioFrameObserver); + rtcEngine_ = rtcEngine; + rtcEngine_->setAudioProfile(AUDIO_PROFILE_MUSIC_STANDARD_STEREO, AUDIO_SCENARIO_CHATROOM_ENTERTAINMENT); + rtcEngine_->setPlaybackAudioFrameParameters(48000, 2, RAW_AUDIO_FRAME_OP_MODE_READ_WRITE, 1920); + rtcEngine_->setRecordingAudioFrameParameters(48000, 2, RAW_AUDIO_FRAME_OP_MODE_READ_WRITE, 1920); + this->player_ = player; + agora::util::AutoPtr mediaEngine; + mediaEngine.queryInterface(rtcEngine, agora::AGORA_IID_MEDIA_ENGINE); + if (mediaEngine) { + mediaEngine->registerAudioFrameObserver(audioFrameObserver_.get()); + mediaEngine->setExternalVideoSource(true, false); + } + player_->registerAudioFrameObserver(this); + player_->registerVideoFrameObserver(this); + + } + virtual void publishAudio(){ + is_push_audio_ = true; + } + virtual void publishVideo(){ + is_push_video_ = true; + } + virtual void unpublishVideo(){ + is_push_video_ = false; + } + virtual void unpublishAudio(){ + is_push_audio_ = false; + } + virtual void registerMediaPlayerObserver(AgoraMediaPlayerObserver * observer){ + player_->registerPlayerObserver(observer); + } + virtual void adjustPlayoutSignalVolume(int volume){ + audioFrameObserver_->setPlayoutSignalVolume(volume); + } + virtual void adjustPublishSignalVolume(int volume){ + audioFrameObserver_->setPublishSignalVolume(volume); + } + ~AgoraMediaPlayerEx(){ + + } +private: + agora::rtc::IMediaPlayer *player_; + std::unique_ptr audioFrameObserver_; + agora::rtc::IRtcEngine *rtcEngine_; + std::atomic is_push_audio_{false}; + std::atomic is_push_video_{false}; + IMediaPlayerObserver *observer_; +}; +IAgoraMediaPlayerEx *createAgoraMediaPlayerFactory(){ + return new AgoraMediaPlayerEx; +} +#endif diff --git a/iOS/APIExample/Common/RtcChannelPublishPlugin/AgoraMediaPlayerEx.h b/iOS/APIExample/Common/RtcChannelPublishPlugin/AgoraMediaPlayerEx.h new file mode 100644 index 000000000..807b3cdec --- /dev/null +++ b/iOS/APIExample/Common/RtcChannelPublishPlugin/AgoraMediaPlayerEx.h @@ -0,0 +1,83 @@ +// +// AgoraMediaPlayerEx.hpp +// player_demo_apple +// +// Created by zhanxiaochao on 2020/5/26. +// Copyright © 2020 agora. All rights reserved. +// +#ifdef MEDIAPLAYER +#ifndef AgoraMediaPlayerEx_h +#define AgoraMediaPlayerEx_h +#include +#include +#import +#import +class AgoraMediaPlayerObserver : public agora::rtc::IMediaPlayerObserver +{ + ; + /** + * @brief Triggered when the player state changes + * + * @param state New player state + * @param ec Player error message + */ + virtual void onPlayerStateChanged(agora::media::MEDIA_PLAYER_STATE state, + agora::media::MEDIA_PLAYER_ERROR ec) + { + + } + + /** + * @brief Triggered when the player progress changes, once every 1 second + * + * @param position Current playback progress, in seconds + */ + virtual void onPositionChanged(const int64_t position) + { + + } + /** + * @brief Triggered when the player have some event + * + * @param event media player event + */ + virtual void onPlayerEvent(agora::media::MEDIA_PLAYER_EVENT event) + { + + }; + + /** + * @brief Triggered when metadata is obtained + * + * @param type Metadata type + * @param data data + * @param length data length + */ + virtual void onMetadata(agora::media::MEDIA_PLAYER_METADATA_TYPE type, const uint8_t* data, + uint32_t length) + { + + } +}; + +class IAgoraMediaPlayerEx{ +public: + virtual void attachMediaPlayer(agora::rtc::IMediaPlayer *player,agora::rtc::IRtcEngine *rtcEngine) = 0; + virtual void registerMediaPlayerObserver(AgoraMediaPlayerObserver * observer) = 0; + virtual void publishAudio() = 0; + virtual void publishVideo() = 0; + virtual void unpublishVideo() = 0; + virtual void unpublishAudio() = 0; + virtual void adjustPlayoutSignalVolume(int volume) = 0; + virtual void adjustPublishSignalVolume(int volume) = 0; + virtual void detachPlayerFromRtc() = 0; + virtual ~IAgoraMediaPlayerEx() = default; + +}; +IAgoraMediaPlayerEx * createAgoraMediaPlayerFactory(); + + + + +#endif /* AgoraMediaPlayerEx_hpp */ +#endif diff --git a/iOS/APIExample/Common/RtcChannelPublishPlugin/AgoraRtcChannelPublishHelper.h b/iOS/APIExample/Common/RtcChannelPublishPlugin/AgoraRtcChannelPublishHelper.h new file mode 100644 index 000000000..c80911dfa --- /dev/null +++ b/iOS/APIExample/Common/RtcChannelPublishPlugin/AgoraRtcChannelPublishHelper.h @@ -0,0 +1,69 @@ +// +// AgoraRtcChannelPublishHelper.h +// player_demo_apple +// +// Created by zhanxiaochao on 2020/1/13. +// Copyright © 2020 agora. All rights reserved. +// + +#import +#import +#import +NS_ASSUME_NONNULL_BEGIN +@protocol AgoraRtcChannelPublishHelperDelegate + +@optional + +/// Description of state of Mediaplayer's state +/// @param playerKit AgoraMediaPlayer +/// @param state AgoraMediaPlayerState +/// @param reason AgoraMediaPlayerStateReason +/// @param error AgoraMediaPlayerError +- (void)AgoraRtcChannelPublishHelperDelegate:(AgoraMediaPlayer *_Nonnull)playerKit + didChangedToState:(AgoraMediaPlayerState)state + error:(AgoraMediaPlayerError)error; + +/// callback of position +/// @param playerKit AgoraMediaPlayer +/// @param position position +- (void)AgoraRtcChannelPublishHelperDelegate:(AgoraMediaPlayer *_Nonnull)playerKit + didChangedToPosition:(NSInteger)position; + +/// callback of seek state +/// @param playerkit AgoraMediaPlayer +/// @param state Description of seek state +- (void)AgoraRtcChannelPublishHelperDelegate:(AgoraMediaPlayer *_Nonnull)playerKit + didOccureEvent:(AgoraMediaPlayerEvent)state; + +/// callback of SEI +/// @param playerkit AgoraMediaPlayer +/// @param data SEI's data +- (void)AgoraRtcChannelPublishHelperDelegate:(AgoraMediaPlayer *_Nonnull)playerKit + didReceiveData:(NSString *)data + length:(NSInteger)length; + +@end + +@interface AgoraRtcChannelPublishHelper : NSObject + ++(instancetype)shareInstance; +// 连接 MediaPlayer 到主版本 RTC SDK +- (void)registerRtcChannelPublishHelperDelegate:(id)delegate; +- (void)attachPlayerToRtc:(AgoraMediaPlayer *)playerKit RtcEngine:(AgoraRtcEngineKit *)rtcEngine enableVideoSource:(bool)enable; +- (void)enableOnlyLocalAudioPlay:(bool)isEnable; +// 启动/停止推送音频流到频道 +- (void)publishAudio; +- (void)unpublishAudio; +// 启动/停止推送视频流到频道 +- (void)publishVideo; +- (void)unpublishVideo; +// 调节推送到频道内音频流的音量 +- (void)adjustPublishSignalVolume:(int)volume; +// 调节播放视频的声音 +- (void)adjustPlayoutSignalVolume:(int)volume; +// 断开 MediaPlayer 和 RTC SDK 的关联 +- (void)detachPlayerFromRtc; + +@end + +NS_ASSUME_NONNULL_END diff --git a/iOS/APIExample/Common/RtcChannelPublishPlugin/AgoraRtcChannelPublishHelper.mm b/iOS/APIExample/Common/RtcChannelPublishPlugin/AgoraRtcChannelPublishHelper.mm new file mode 100644 index 000000000..4c3229824 --- /dev/null +++ b/iOS/APIExample/Common/RtcChannelPublishPlugin/AgoraRtcChannelPublishHelper.mm @@ -0,0 +1,449 @@ +// +// AgoraRtcChannelPublishHelper.m +// player_demo_apple +// +// Created by zhanxiaochao on 2020/1/13. +// Copyright © 2020 agora. All rights reserved. +// + +#import "AgoraRtcChannelPublishHelper.h" +#import +#import +#import "AudioCircularBuffer.h" +#import "scoped_ptr.h" +#import +using namespace AgoraRTC; +static NSObject *threadLockPush = [[NSObject alloc] init]; +static NSObject *threadLockPlay = [[NSObject alloc] init]; + +class AgoraAudioFrameObserver:public agora::media::IAudioFrameObserver +{ +private: + int16_t * record_buf_tmp_ = nullptr; + char * record_audio_mix_ = nullptr; + int16_t * record_send_buf_ = nullptr; + + int16_t * play_buf_tmp_ = nullptr; + char * play_audio_mix_ = nullptr; + int16_t * play_send_buf_ = nullptr; + scoped_ptr> record_audio_buf_; + scoped_ptr> play_audio_buf_; +public: + std::atomic publishSignalValue_{1.0f}; + std::atomic playOutSignalValue_{1.0f}; + std::atomic isOnlyAudioPlay_{false}; + AgoraAudioFrameObserver(){ + record_audio_buf_.reset(new AudioCircularBuffer(true,2048)); + play_audio_buf_.reset(new AudioCircularBuffer(true,2048)); + } + ~AgoraAudioFrameObserver() + { + if (record_buf_tmp_) { + free(record_buf_tmp_); + } + if(record_audio_mix_){ + free(record_audio_mix_); + } + if(record_send_buf_){ + free(record_send_buf_); + } + + if (play_buf_tmp_) { + free(play_buf_tmp_); + } + if(play_audio_mix_){ + free(play_audio_mix_); + } + if (play_send_buf_) { + free(play_send_buf_); + } + } + void resetAudioBuffer(){ + + record_audio_buf_.reset(new AudioCircularBuffer(2048,true)); + play_audio_buf_.reset(new AudioCircularBuffer(2048,true)); + } + void setPublishSignalVolume(int volume){ + @synchronized (threadLockPush) { + publishSignalValue_ = volume/100.0f; + } + } + void enableOnlyAudioPlay(bool isEnable){ + isOnlyAudioPlay_ = isEnable; + } + void setPlayoutSignalVolume(int volume){ + @synchronized (threadLockPlay) { + playOutSignalValue_ = volume/100.0f; + } + } + void pushData(char *data,int length){ + { + if (!isOnlyAudioPlay_) { + record_audio_buf_->Push(data, length); + } + } + { + play_audio_buf_->Push(data, length); + } + + } + virtual bool onRecordAudioFrame(AudioFrame& audioFrame){ + @synchronized (threadLockPush) { + int bytes = audioFrame.samples * audioFrame.channels * audioFrame.bytesPerSample; + int ret = record_audio_buf_->mAvailSamples - bytes; + if ( ret < 0) { + return false; + } + //计算重采样钱的数据大小 重采样的采样率 * SDK回调时间 * 声道数 * 字节数 + if (!record_buf_tmp_) { + record_buf_tmp_ = (int16_t *)malloc(bytes); + } + if(!record_audio_mix_){ + record_audio_mix_ = (char *)malloc(bytes); + } + if(!record_send_buf_){ + record_send_buf_ = (int16_t *)malloc(bytes); + } + record_audio_buf_->Pop(record_audio_mix_, bytes); + int16_t* p16 = (int16_t*) record_audio_mix_; + memcpy(record_buf_tmp_, audioFrame.buffer, bytes); + for (int i = 0; i < bytes / 2; ++i) { + record_buf_tmp_[i] += (p16[i] * publishSignalValue_); + //audio overflow + if (record_buf_tmp_[i] > 32767) { + record_send_buf_[i] = 32767; + } + else if (record_buf_tmp_[i] < -32768) { + record_send_buf_[i] = -32768; + } + else { + record_send_buf_[i] = record_buf_tmp_[i]; + } + } + memcpy(audioFrame.buffer, record_send_buf_,bytes); + } + return true; + } + /** + * Occurs when the playback audio frame is received. + * @param audioframe The reference to the audio frame: AudioFrame. + * @return + * - true: The playback audio frame is valid and is encoded and sent. + * - false: The playback audio frame is invalid and is not encoded or sent. + */ + virtual bool onPlaybackAudioFrame(AudioFrame& audioFrame){ + @synchronized (threadLockPlay) { + + int bytes = audioFrame.samples * audioFrame.channels * audioFrame.bytesPerSample; + int ret = play_audio_buf_->mAvailSamples - bytes; + if (ret < 0) { + return false; + } + //计算重采样钱的数据大小 重采样的采样率 * SDK回调时间 * 声道数 * 字节数 + if(!play_buf_tmp_){ + play_buf_tmp_ = (int16_t *)malloc(bytes); + } + if(!play_audio_mix_){ + play_audio_mix_ = (char *)malloc(bytes); + } + if(!play_send_buf_){ + play_send_buf_ = (int16_t *)malloc(bytes); + } + play_audio_buf_->Pop(play_audio_mix_, bytes); + int16_t* p16 = (int16_t*) play_audio_mix_; + memcpy(play_buf_tmp_, audioFrame.buffer, bytes); + for (int i = 0; i < bytes / 2; ++i) { + play_buf_tmp_[i] += (p16[i] * playOutSignalValue_); + //audio overflow + if (play_buf_tmp_[i] > 32767) { + play_send_buf_[i] = 32767; + } + else if (play_buf_tmp_[i] < -32768) { + play_send_buf_[i] = -32768; + } + else { + play_send_buf_[i] = play_buf_tmp_[i]; + } + } + memcpy(audioFrame.buffer, play_buf_tmp_,bytes); + } + return true; + } + /** + * Occurs when the mixed audio data is received. + * @param audioframe The reference to the audio frame: AudioFrame. + * @return + * - true: The mixed audio data is valid and is encoded and sent. + * - false: The mixed audio data is invalid and is not encoded or sent. + */ + virtual bool onMixedAudioFrame(AudioFrame& audioFrame){ + return false; + } + /** + * Occurs when the playback audio frame before mixing is received. + * @param audioframe The reference to the audio frame: AudioFrame. + * @return + * - true: The playback audio frame before mixing is valid and is encoded and sent. + * - false: The playback audio frame before mixing is invalid and is not encoded or sent. + */ + virtual bool onPlaybackAudioFrameBeforeMixing(unsigned int uid, AudioFrame& audioFrame){ + return false; + } +}; +@interface AgoraRtcChannelPublishHelper() +{ + std::unique_ptr audioFrameObserver; + BOOL isPublishVideo; +} +@property (nonatomic, weak)AgoraMediaPlayer *playerKit; +@property (nonatomic, weak)AgoraRtcEngineKit *rtcEngineKit; +@property (nonatomic, weak)id delegate; +@property (nonatomic, assign)bool isDispatchMainQueue; + +@end +@implementation AgoraRtcChannelPublishHelper + +static AgoraRtcChannelPublishHelper *instance = NULL; ++ (instancetype)shareInstance{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + if (instance == NULL) { + instance = [[AgoraRtcChannelPublishHelper alloc] init]; + } + }); + return instance; +} +// 连接 MediaPlayer 到主版本 RTC SDK +- (void)attachPlayerToRtc:(AgoraMediaPlayer *)playerKit RtcEngine:(AgoraRtcEngineKit *)rtcEngine enableVideoSource:(bool)enable{ + audioFrameObserver = std::make_unique(); + isPublishVideo = false; + audioFrameObserver->setPublishSignalVolume(0); + self.isDispatchMainQueue = false; + playerKit.delegate = self; + if (enable) { + [rtcEngine setVideoSource:self]; + } + [rtcEngine setParameters:@"{\"che.audio.keep.audiosession\":true}"]; + [rtcEngine setAudioProfile:AgoraAudioProfileMusicStandardStereo scenario:AgoraAudioScenarioChatRoomEntertainment]; + [rtcEngine setRecordingAudioFrameParametersWithSampleRate:48000 channel:2 mode:AgoraAudioRawFrameOperationModeReadWrite samplesPerCall:960]; + [rtcEngine setPlaybackAudioFrameParametersWithSampleRate:48000 channel:2 mode:AgoraAudioRawFrameOperationModeReadWrite samplesPerCall:960]; + + [self registerRtcEngine:rtcEngine]; + _playerKit = playerKit; + _rtcEngineKit = rtcEngine; + [self resetAudioBuf]; +} +// 启动/停止推送音频流到频道 +- (void)publishAudio{ + @synchronized (self) { + audioFrameObserver->setPublishSignalVolume(100); + } +} +- (void)unpublishAudio{ + @synchronized (self) { + audioFrameObserver->setPublishSignalVolume(0); + [self resetAudioBuf]; + } + +} +- (void)enableOnlyLocalAudioPlay:(bool)isEnable + +{ + @synchronized (self) { + audioFrameObserver->enableOnlyAudioPlay(isEnable); + } +} +// 启动/停止推送视频流到频道 +- (void)publishVideo{ + @synchronized (self) { + isPublishVideo = true; + } +} +- (void)unpublishVideo{ + + @synchronized (self) { + isPublishVideo = false; + } +} +// 调节推送到频道内音频流的音量 +- (void)adjustPublishSignalVolume:(int)volume{ + + @synchronized (self) { + audioFrameObserver->setPublishSignalVolume(volume); + } +} +-(void)adjustPlayoutSignalVolume:(int)volume +{ @synchronized (self) { + audioFrameObserver->setPlayoutSignalVolume(volume); + } +} +// 断开 MediaPlayer 和 RTC SDK 的关联 +- (void)detachPlayerFromRtc{ + @synchronized (self) { + isPublishVideo=false; + audioFrameObserver->setPublishSignalVolume(0); + [self unregisterRtcEngine:_rtcEngineKit]; + [_rtcEngineKit setVideoSource:NULL]; + _playerKit.delegate = NULL; + + } +} +- (void)resetAudioBuf{ + @synchronized (self) { + audioFrameObserver->resetAudioBuffer(); + } +} +- (void)AgoraMediaPlayer:(AgoraMediaPlayer *_Nonnull)playerKit + didReceiveVideoFrame:(CVPixelBufferRef)pixelBuffer{ + @synchronized (self) { + if (!isPublishVideo) { + return; + } + //pushExternalCVPixelBuffer + [self.consumer consumePixelBuffer:pixelBuffer withTimestamp:CMTimeMake(CACurrentMediaTime()*1000, 1000) rotation:AgoraVideoRotationNone]; + + } + +} +- (void)registerRtcEngine:(AgoraRtcEngineKit *)rtcEngine +{ + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)rtcEngine.getNativeHandle; + agora::util::AutoPtr mediaEngine; + mediaEngine.queryInterface(rtc_engine, agora::AGORA_IID_MEDIA_ENGINE); + if (mediaEngine) { + mediaEngine->registerAudioFrameObserver(audioFrameObserver.get()); + } +} +- (void)unregisterRtcEngine:(AgoraRtcEngineKit *)rtcEngine +{ + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)rtcEngine.getNativeHandle; + agora::util::AutoPtr mediaEngine; + mediaEngine.queryInterface(rtc_engine, agora::AGORA_IID_MEDIA_ENGINE); + if (mediaEngine) { + mediaEngine->registerAudioFrameObserver(NULL); + } +} + +- (void)AgoraMediaPlayer:(AgoraMediaPlayer *_Nonnull)playerKit + didReceiveAudioFrame:(CMSampleBufferRef)audioFrame{ + //pushExternalAudioBuffer + CMBlockBufferRef audioBuffer = CMSampleBufferGetDataBuffer(audioFrame); + OSStatus err; + size_t lengthAtOffSet; + size_t totalBytes; + char *samples; + err = CMBlockBufferGetDataPointer(audioBuffer, 0, &lengthAtOffSet, &totalBytes, &samples); + if (totalBytes == 0) { + return; + } + audioFrameObserver->pushData(samples, (int)totalBytes); + +} +@synthesize consumer; + +- (AgoraVideoBufferType)bufferType { + return AgoraVideoBufferTypePixelBuffer; +} + +- (void)shouldDispose { + +} + +- (BOOL)shouldInitialize { + return true; +} + +- (void)shouldStart { + +} + +- (void)shouldStop { + +} + +/// Description of state of Mediaplayer's state +/// @param playerKit AgoraMediaPlayer +/// @param state AgoraMediaPlayerState +/// @param reason AgoraMediaPlayerStateReason +/// @param error AgoraMediaPlayerError +- (void)AgoraMediaPlayer:(AgoraMediaPlayer *_Nonnull)playerKit + didChangedToState:(AgoraMediaPlayerState)state + error:(AgoraMediaPlayerError)error +{ + + if (self.delegate && [self.delegate respondsToSelector:@selector(AgoraRtcChannelPublishHelperDelegate:didChangedToState:error:)]) { + __weak typeof(self) weakSelf = self; + [self executeBlock:^{ + if (state == AgoraMediaPlayerStateOpenCompleted) { + [weakSelf.playerKit mute:true]; + [weakSelf resetAudioBuf]; + } + [self.delegate AgoraRtcChannelPublishHelperDelegate:weakSelf.playerKit didChangedToState:state error:error]; + }]; + } + +} + +/// callback of position +/// @param playerKit AgoraMediaPlayer +/// @param position position +- (void)AgoraMediaPlayer:(AgoraMediaPlayer *_Nonnull)playerKit + didChangedToPosition:(NSInteger)position +{ + if (self.delegate && [self.delegate respondsToSelector:@selector(AgoraRtcChannelPublishHelperDelegate:didChangedToPosition:)]) { + __weak typeof(self) weakSelf = self; + [self executeBlock:^{ + [self.delegate AgoraRtcChannelPublishHelperDelegate:weakSelf.playerKit didChangedToPosition:position]; + }]; + } +} + +/// callback of seek state +/// @param playerkit AgoraMediaPlayer +/// @param state Description of seek state +- (void)AgoraMediaPlayer:(AgoraMediaPlayer *)playerKit didOccurEvent:(AgoraMediaPlayerEvent)event +{ + if (self.delegate && [self.delegate respondsToSelector:@selector(AgoraRtcChannelPublishHelperDelegate:didOccureEvent:)]) { + __weak typeof(self) weakSelf = self; + [self executeBlock:^{ + if (event == AgoraMediaPlayerEventSeekComplete) { + [weakSelf resetAudioBuf]; + } + [self.delegate AgoraRtcChannelPublishHelperDelegate:weakSelf.playerKit didOccureEvent:event]; + }]; + } + +} + +/// callback of SEI +/// @param playerkit AgoraMediaPlayer +/// @param data SEI's data +- (void)AgoraMediaPlayer:(AgoraMediaPlayer *)playerKit metaDataType:(AgoraMediaPlayerMetaDataType)type didReceiveData:(NSString *)data length:(NSInteger)length{ + if (self.delegate && [self.delegate respondsToSelector:@selector(AgoraRtcChannelPublishHelperDelegate:didReceiveData:length:)]) { + __weak typeof(self) weakSelf = self; + [self executeBlock:^{ + [self.delegate AgoraRtcChannelPublishHelperDelegate:weakSelf.playerKit didReceiveData:data length:length]; + }]; + } + +} +- (void)registerRtcChannelPublishHelperDelegate:(id)delegate{ + @synchronized (self) { + self.delegate = delegate; + } +} +- (void)executeBlock:(void (^)())block { + if (self.isDispatchMainQueue) { + dispatch_async(dispatch_get_main_queue(), ^{ + block(); + }); + } else { + dispatch_async(dispatch_get_global_queue(0, 0), ^{ + block(); + }); + } +} + +@end + + diff --git a/iOS/APIExample/Common/RtcChannelPublishPlugin/AudioFrameObserver/AudioFrameObserver.cpp b/iOS/APIExample/Common/RtcChannelPublishPlugin/AudioFrameObserver/AudioFrameObserver.cpp new file mode 100644 index 000000000..cb67a36e9 --- /dev/null +++ b/iOS/APIExample/Common/RtcChannelPublishPlugin/AudioFrameObserver/AudioFrameObserver.cpp @@ -0,0 +1,9 @@ +// +// AudioFrameObserver.cpp +// player_demo_apple +// +// Created by zhanxiaochao on 2020/5/27. +// Copyright © 2020 agora. All rights reserved. +// + +#include "AudioFrameObserver.h" diff --git a/iOS/APIExample/Common/RtcChannelPublishPlugin/AudioFrameObserver/AudioFrameObserver.h b/iOS/APIExample/Common/RtcChannelPublishPlugin/AudioFrameObserver/AudioFrameObserver.h new file mode 100644 index 000000000..61b654669 --- /dev/null +++ b/iOS/APIExample/Common/RtcChannelPublishPlugin/AudioFrameObserver/AudioFrameObserver.h @@ -0,0 +1,147 @@ +// +// AudioFrameObserver.hpp +// player_demo_apple +// +// Created by zhanxiaochao on 2020/5/27. +// Copyright © 2020 agora. All rights reserved. +// + +#ifndef AudioFrameObserver_h +#define AudioFrameObserver_h +#include +#import +#import +#import "AudioCircularBuffer.h" +#import "scoped_ptr.h" +using namespace AgoraRTC; +class AgoraAudioFrameObserver:public agora::media::IAudioFrameObserver +{ +public: + std::atomic publishSignalValue_{1.0f}; + std::atomic playOutSignalValue_{1.0f}; + scoped_ptr> agoraAudioBuf; + scoped_ptr> agoraPlayoutBuf; + AgoraAudioFrameObserver(){ + agoraAudioBuf.reset(new AudioCircularBuffer(2048,true)); + agoraPlayoutBuf.reset(new AudioCircularBuffer(2048,true)); + } + void setPublishSignalVolume(int volume){ + publishSignalValue_ = volume/100.0f; + } + void setPlayoutSignalVolume(int volume){ + playOutSignalValue_ = volume/100.0f; + } + void pushData(char *data,int length){ + agoraAudioBuf->Push(data, length); + agoraPlayoutBuf->Push(data, length); + + } + void resetAudioBuf(){ + agoraAudioBuf.reset(new AudioCircularBuffer(2048,true)); + agoraPlayoutBuf.reset(new AudioCircularBuffer(2048,true)); + } + virtual bool onRecordAudioFrame(AudioFrame& audioFrame){ + + int bytes = audioFrame.samples * audioFrame.channels * audioFrame.bytesPerSample; + int16_t *tmpBuf = (int16_t *)malloc(sizeof(int16_t)*bytes); + memcpy(tmpBuf, audioFrame.buffer, bytes); + if (agoraAudioBuf->mAvailSamples < bytes) { + memcpy(audioFrame.buffer, tmpBuf, sizeof(int16_t)*bytes); + free(tmpBuf); + return true; + } + //计算重采样钱的数据大小 重采样的采样率 * SDK回调时间 * 声道数 * 字节数 + int mv_size = bytes; + char *data = (char *)malloc(sizeof(char)*mv_size); + agoraAudioBuf->Pop(data, mv_size); + int16_t* p16 = (int16_t*) data; + int16_t *audioBuf = (int16_t *)malloc(bytes); + memcpy(audioBuf, tmpBuf, bytes); + for (int i = 0; i < bytes / 2; ++i) { + tmpBuf[i] += (p16[i] * publishSignalValue_); + //audio overflow + if (tmpBuf[i] > 32767) { + audioBuf[i] = 32767; + } + else if (tmpBuf[i] < -32768) { + audioBuf[i] = -32768; + } + else { + audioBuf[i] = tmpBuf[i]; + } + } + memcpy(audioFrame.buffer, audioBuf,bytes); + free(audioBuf); + free(tmpBuf); + free(p16); + return true; + } + /** + * Occurs when the playback audio frame is received. + * @param audioframe The reference to the audio frame: AudioFrame. + * @return + * - true: The playback audio frame is valid and is encoded and sent. + * - false: The playback audio frame is invalid and is not encoded or sent. + */ + virtual bool onPlaybackAudioFrame(AudioFrame& audioFrame){ + int bytes = audioFrame.samples * audioFrame.channels * audioFrame.bytesPerSample; + int16_t *tmpBuf = (int16_t *)malloc(bytes); + memcpy(tmpBuf, audioFrame.buffer, bytes); + if (agoraPlayoutBuf->mAvailSamples < bytes) { + memcpy(audioFrame.buffer, tmpBuf,bytes); + free(tmpBuf); + return true; + } + //计算重采样钱的数据大小 重采样的采样率 * SDK回调时间 * 声道数 * 字节数 + int mv_size = bytes; + char *data = (char *)malloc(mv_size); + agoraPlayoutBuf->Pop(data, mv_size); + int16_t* p16 = (int16_t*) data; + int16_t *audioBuf = (int16_t *)malloc(bytes); + memcpy(audioBuf, tmpBuf, bytes); + for (int i = 0; i < bytes / 2; ++i) { + tmpBuf[i] += (p16[i] * playOutSignalValue_); + //audio overflow + if (tmpBuf[i] > 32767) { + audioBuf[i] = 32767; + } + else if (tmpBuf[i] < -32768) { + audioBuf[i] = -32768; + } + else { + audioBuf[i] = tmpBuf[i]; + } + } + memcpy(audioFrame.buffer, audioBuf,bytes); + free(audioBuf); + free(tmpBuf); + free(p16); + return true; + } + /** + * Occurs when the mixed audio data is received. + * @param audioframe The reference to the audio frame: AudioFrame. + * @return + * - true: The mixed audio data is valid and is encoded and sent. + * - false: The mixed audio data is invalid and is not encoded or sent. + */ + virtual bool onMixedAudioFrame(AudioFrame& audioFrame){ + return false; + } + /** + * Occurs when the playback audio frame before mixing is received. + * @param audioframe The reference to the audio frame: AudioFrame. + * @return + * - true: The playback audio frame before mixing is valid and is encoded and sent. + * - false: The playback audio frame before mixing is invalid and is not encoded or sent. + */ + virtual bool onPlaybackAudioFrameBeforeMixing(unsigned int uid, AudioFrame& audioFrame){ + return false; + } +}; + + + + + +#endif /* AudioFrameObserver_hpp */ diff --git a/iOS/APIExample/Common/RtcChannelPublishPlugin/utils/AudioCircularBuffer.cc b/iOS/APIExample/Common/RtcChannelPublishPlugin/utils/AudioCircularBuffer.cc new file mode 100755 index 000000000..3cb6fa3cb --- /dev/null +++ b/iOS/APIExample/Common/RtcChannelPublishPlugin/utils/AudioCircularBuffer.cc @@ -0,0 +1,16 @@ +/* +* Copyright (c) 2016 The Agora project authors. All Rights Reserved. +* +* Use of this source code is governed by a BSD-style license +* that can be found in the LICENSE file in the root of the source +* tree. An additional intellectual property rights grant can be found +* in the file PATENTS. All contributing project authors may +* be found in the AUTHORS file in the root of the source tree. +*/ + +#include "AudioCircularBuffer.h" +#include + + + + diff --git a/iOS/APIExample/Common/RtcChannelPublishPlugin/utils/AudioCircularBuffer.h b/iOS/APIExample/Common/RtcChannelPublishPlugin/utils/AudioCircularBuffer.h new file mode 100755 index 000000000..23830dc62 --- /dev/null +++ b/iOS/APIExample/Common/RtcChannelPublishPlugin/utils/AudioCircularBuffer.h @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2016 The Agora project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef WEBRTC_CHAT_ENGINE_FILE_AUDIO_CIRCULAR_BUFFER_H_ +#define WEBRTC_CHAT_ENGINE_FILE_AUDIO_CIRCULAR_BUFFER_H_ + +#include "scoped_ptr.h" +#include +#include + + + +template + +class AudioCircularBuffer { + + public: + typedef Ty value; + AudioCircularBuffer(uint32_t initSize, bool newWay) + : pInt16BufferPtr(nullptr), + bNewWayProcessing(newWay) + { + std::lock_guard _(mtx_); + mInt16BufferLength = initSize; + if (bNewWayProcessing) { + pInt16BufferPtr = new value[sizeof(value) * mInt16BufferLength]; + } + else { + if (!pInt16Buffer.get()) { + pInt16Buffer.reset(new value[sizeof(value) * mInt16BufferLength]); + } + } + } + + ~AudioCircularBuffer() + { + std::lock_guard _(mtx_); + if (pInt16BufferPtr) { + delete [] pInt16BufferPtr; + pInt16BufferPtr = nullptr; + } + } + + void Push(value* data, int length) + { + std::lock_guard _(mtx_); + if (bNewWayProcessing) { + // If the internal buffer is not large enough, first enlarge the buffer + if (mAvailSamples + length > mInt16BufferLength) { + int newLength = std::max(length + mAvailSamples + 960, 2 * mInt16BufferLength); + value * tmpBuffer = new value[sizeof(value) * newLength]; + if (mReadPtrPosition + mAvailSamples > mInt16BufferLength) { + int firstCopyLength = mInt16BufferLength - mReadPtrPosition; + + memcpy(tmpBuffer, pInt16BufferPtr + mReadPtrPosition, sizeof(value) * firstCopyLength); + memcpy(tmpBuffer + firstCopyLength, pInt16BufferPtr, sizeof(value) * (mAvailSamples - firstCopyLength)); + } + else { + memcpy(tmpBuffer, pInt16BufferPtr + mReadPtrPosition, sizeof(value) * mAvailSamples); + } + delete [] pInt16BufferPtr; + + // Construct the new internal array + mInt16BufferLength = newLength; + pInt16BufferPtr = tmpBuffer; + mReadPtrPosition = 0; + mWritePtrPosition = mAvailSamples; + memcpy(pInt16BufferPtr + mWritePtrPosition, data, sizeof(value) * length); + mWritePtrPosition += length; + } + else { + int availSlots = mInt16BufferLength - mWritePtrPosition; + if (availSlots < length) { + memcpy(pInt16BufferPtr + mWritePtrPosition, data, sizeof(value) * availSlots); + memcpy(pInt16BufferPtr, data + availSlots, sizeof(value) * (length - availSlots)); + } + else { + memcpy(pInt16BufferPtr + mWritePtrPosition, data, sizeof(value)*length); + } + mWritePtrPosition = IntModule(mWritePtrPosition, length, mInt16BufferLength); + } + mAvailSamples += length; + } + else { + // If the internal buffer is not large enough, first enlarge the buffer + if (length + mAvailSamples > mInt16BufferLength) { + value * tmpBuffer = new value[sizeof(value) * mAvailSamples]; + memmove(tmpBuffer, &pInt16Buffer[mReadPtrPosition], sizeof(value)*mAvailSamples); + + mInt16BufferLength = (length + mAvailSamples) * 2; + pInt16Buffer.reset(new value[sizeof(value) * mInt16BufferLength]); + memmove(&pInt16Buffer[0], tmpBuffer, sizeof(value)*mAvailSamples); + + delete[] tmpBuffer; + mReadPtrPosition = 0; + } + else { + memmove(&pInt16Buffer[0], &pInt16Buffer[mReadPtrPosition], sizeof(value)*mAvailSamples); + } + + memmove(&pInt16Buffer[mAvailSamples], data, sizeof(value)*length); + mAvailSamples += length; + mReadPtrPosition = 0; + } + } + + void Pop(value* data, int length) + { + std::lock_guard _(mtx_); + if (bNewWayProcessing) { + int availSlots = mInt16BufferLength - mReadPtrPosition; + if (availSlots < length) { + memcpy(data, pInt16BufferPtr + mReadPtrPosition, sizeof(value) * availSlots); + memcpy(data + availSlots, pInt16BufferPtr, sizeof(value) * (length - availSlots)); + } + else { + memcpy(data, pInt16BufferPtr + mReadPtrPosition, sizeof(value)*length); + } + mReadPtrPosition = IntModule(mReadPtrPosition, length, mInt16BufferLength); + mAvailSamples -= length; + } + else { + memmove(data, &pInt16Buffer[mReadPtrPosition], sizeof(value)*length); + mAvailSamples -= length; + mReadPtrPosition += length; + } + } + + void Discard(int length) + { + if (bNewWayProcessing) { + mReadPtrPosition = IntModule(mReadPtrPosition, length, mInt16BufferLength); + mAvailSamples -= length; + } + else { + mAvailSamples -= length; + mReadPtrPosition += length; + } + } + + void Reset() + { + std::lock_guard _(mtx_); + mAvailSamples = 0; + mReadPtrPosition = 0; + mWritePtrPosition = 0; + } + + bool dataAvailable(uint32_t requireLength) { + return mAvailSamples >= requireLength; + } + static uint32_t IntModule(uint32_t ptrIndex, int frmLength, int bufLength) + { + if (ptrIndex + frmLength >= bufLength) { + return ptrIndex + frmLength - bufLength; + } + else { + return ptrIndex + frmLength; + } + } + uint32_t mAvailSamples = 0; + uint32_t mReadPtrPosition = 0; + uint32_t mWritePtrPosition = 0; + uint32_t mInt16BufferLength; + value* pInt16BufferPtr; + AgoraRTC::scoped_array pInt16Buffer; + + private: + std::mutex mtx_; + bool bNewWayProcessing; + + }; +//ptrIndex = (ptrIndex + frmLength) % bufLength + + + +#endif // WEBRTC_CHAT_ENGINE_FILE_AUDIO_CIRCULAR_BUFFER_H_ diff --git a/iOS/APIExample/Common/RtcChannelPublishPlugin/utils/scoped_ptr.h b/iOS/APIExample/Common/RtcChannelPublishPlugin/utils/scoped_ptr.h new file mode 100755 index 000000000..e6b37acba --- /dev/null +++ b/iOS/APIExample/Common/RtcChannelPublishPlugin/utils/scoped_ptr.h @@ -0,0 +1,715 @@ +// (C) Copyright Greg Colvin and Beman Dawes 1998, 1999. +// Copyright (c) 2001, 2002 Peter Dimov +// +// Permission to copy, use, modify, sell and distribute this software +// is granted provided this copyright notice appears in all copies. +// This software is provided "as is" without express or implied +// warranty, and with no claim as to its suitability for any purpose. +// +// See http://www.boost.org/libs/smart_ptr/scoped_ptr.htm for documentation. +// + +// scoped_ptr mimics a built-in pointer except that it guarantees deletion +// of the object pointed to, either on destruction of the scoped_ptr or via +// an explicit reset(). scoped_ptr is a simple solution for simple needs; +// use shared_ptr or std::auto_ptr if your needs are more complex. + +// scoped_ptr_malloc added in by Google. When one of +// these goes out of scope, instead of doing a delete or delete[], it +// calls free(). scoped_ptr_malloc is likely to see much more +// use than any other specializations. + +// release() added in by Google. Use this to conditionally +// transfer ownership of a heap-allocated object to the caller, usually on +// method success. +#ifndef WEBRTC_SYSTEM_WRAPPERS_INTERFACE_SCOPED_PTR_H_ +#define WEBRTC_SYSTEM_WRAPPERS_INTERFACE_SCOPED_PTR_H_ + +#include // for assert +#include // for ptrdiff_t +#include // for free() decl +#include "template_util.h" +#include // for std::swap + +#ifdef _WIN32 +namespace std { using ::ptrdiff_t; }; +#endif // _WIN32 + +namespace AgoraRTC { + +// Function object which deletes its parameter, which must be a pointer. +// If C is an array type, invokes 'delete[]' on the parameter; otherwise, +// invokes 'delete'. The default deleter for scoped_ptr. +template +struct DefaultDeleter { + DefaultDeleter() {} + template DefaultDeleter(const DefaultDeleter& other) { + // IMPLEMENTATION NOTE: C++11 20.7.1.1.2p2 only provides this constructor + // if U* is implicitly convertible to T* and U is not an array type. + // + // Correct implementation should use SFINAE to disable this + // constructor. However, since there are no other 1-argument constructors, + // using a static_assert based on is_convertible<> and requiring + // complete types is simpler and will cause compile failures for equivalent + // misuses. + // + // Note, the is_convertible check also ensures that U is not an + // array. T is guaranteed to be a non-array, so any U* where U is an array + // cannot convert to T*. + enum { T_must_be_complete = sizeof(T) }; + enum { U_must_be_complete = sizeof(U) }; + static_assert(is_convertible::value, + "U* must implicitly convert to T*"); + } + inline void operator()(T* ptr) const { + enum { type_must_be_complete = sizeof(T) }; + delete ptr; + } +}; + +// Specialization of DefaultDeleter for array types. +template +struct DefaultDeleter { + inline void operator()(T* ptr) const { + enum { type_must_be_complete = sizeof(T) }; + delete[] ptr; + } + +private: + // Disable this operator for any U != T because it is undefined to execute + // an array delete when the static type of the array mismatches the dynamic + // type. + // + // References: + // C++98 [expr.delete]p3 + // http://cplusplus.github.com/LWG/lwg-defects.html#938 + template void operator()(U* array) const; +}; + +// Function object which invokes 'free' on its parameter, which must be +// a pointer. Can be used to store malloc-allocated pointers in scoped_ptr: +// +// scoped_ptr foo_ptr( +// static_cast(malloc(sizeof(int)))); +struct FreeDeleter { + inline void operator()(void* ptr) const { + free(ptr); + } +}; + +namespace internal { + + template + struct ShouldAbortOnSelfReset { + template + static internal::NoType Test(const typename U::AllowSelfReset*); + + template + static internal::YesType Test(...); + + static const bool value = + sizeof(Test(0)) == sizeof(internal::YesType); + }; + + // Minimal implementation of the core logic of scoped_ptr, suitable for + // reuse in both scoped_ptr and its specializations. + template + class scoped_ptr_impl { + public: + explicit scoped_ptr_impl(T* p) : data_(p) {} + + // Initializer for deleters that have data parameters. + scoped_ptr_impl(T* p, const D& d) : data_(p, d) {} + + // Templated constructor that destructively takes the value from another + // scoped_ptr_impl. + template + scoped_ptr_impl(scoped_ptr_impl* other) + : data_(other->release(), other->get_deleter()) { + // We do not support move-only deleters. We could modify our move + // emulation to have rtc::subtle::move() and rtc::subtle::forward() + // functions that are imperfect emulations of their C++11 equivalents, + // but until there's a requirement, just assume deleters are copyable. + } + + template + void TakeState(scoped_ptr_impl* other) { + // See comment in templated constructor above regarding lack of support + // for move-only deleters. + reset(other->release()); + get_deleter() = other->get_deleter(); + } + + ~scoped_ptr_impl() { + if (data_.ptr != NULL) { + // Not using get_deleter() saves one function call in non-optimized + // builds. + static_cast(data_)(data_.ptr); + } + } + + void reset(T* p) { + // This is a self-reset, which is no longer allowed for default deleters: + // https://crbug.com/162971 + assert(!ShouldAbortOnSelfReset::value || p == NULL || p != data_.ptr); + + // Note that running data_.ptr = p can lead to undefined behavior if + // get_deleter()(get()) deletes this. In order to prevent this, reset() + // should update the stored pointer before deleting its old value. + // + // However, changing reset() to use that behavior may cause current code to + // break in unexpected ways. If the destruction of the owned object + // dereferences the scoped_ptr when it is destroyed by a call to reset(), + // then it will incorrectly dispatch calls to |p| rather than the original + // value of |data_.ptr|. + // + // During the transition period, set the stored pointer to NULL while + // deleting the object. Eventually, this safety check will be removed to + // prevent the scenario initially described from occurring and + // http://crbug.com/176091 can be closed. + T* old = data_.ptr; + data_.ptr = NULL; + if (old != NULL) + static_cast(data_)(old); + data_.ptr = p; + } + + T* get() const { return data_.ptr; } + + D& get_deleter() { return data_; } + const D& get_deleter() const { return data_; } + + void swap(scoped_ptr_impl& p2) { + // Standard swap idiom: 'using std::swap' ensures that std::swap is + // present in the overload set, but we call swap unqualified so that + // any more-specific overloads can be used, if available. + using std::swap; + swap(static_cast(data_), static_cast(p2.data_)); + swap(data_.ptr, p2.data_.ptr); + } + + T* release() { + T* old_ptr = data_.ptr; + data_.ptr = NULL; + return old_ptr; + } + + T** accept() { + reset(NULL); + return &(data_.ptr); + } + + T** use() { + return &(data_.ptr); + } + + private: + // Needed to allow type-converting constructor. + template friend class scoped_ptr_impl; + + // Use the empty base class optimization to allow us to have a D + // member, while avoiding any space overhead for it when D is an + // empty class. See e.g. http://www.cantrip.org/emptyopt.html for a good + // discussion of this technique. + struct Data : public D { + explicit Data(T* ptr_in) : ptr(ptr_in) {} + Data(T* ptr_in, const D& other) : D(other), ptr(ptr_in) {} + T* ptr; + }; + + Data data_; + }; + +} // namespace internal + +template +class scoped_ptr { + private: + + T* ptr; + + scoped_ptr(scoped_ptr const &); + scoped_ptr & operator=(scoped_ptr const &); + + public: + + typedef T element_type; + + explicit scoped_ptr(T* p = NULL): ptr(p) {} + scoped_ptr(scoped_ptr &&rhs) { + ptr = rhs.ptr; + rhs.ptr = NULL; + } + + scoped_ptr& operator=(scoped_ptr &&rhs) { + if (this != &rhs) { + ptr = rhs.ptr; + rhs.ptr = NULL; + } + + return *this; + } + + ~scoped_ptr() { + typedef char type_must_be_complete[sizeof(T)]; + delete ptr; + } + + void reset(T* p = NULL) { + typedef char type_must_be_complete[sizeof(T)]; + + if (ptr != p) { + T* obj = ptr; + ptr = p; + // Delete last, in case obj destructor indirectly results in ~scoped_ptr + delete obj; + } + } + + T& operator*() const { + assert(ptr != NULL); + return *ptr; + } + + T* operator->() const { + assert(ptr != NULL); + return ptr; + } + + T* get() const { + return ptr; + } + + void swap(scoped_ptr & b) { + T* tmp = b.ptr; + b.ptr = ptr; + ptr = tmp; + } + + T* release() { + T* tmp = ptr; + ptr = NULL; + return tmp; + } + + T** accept() { + if (ptr) { + delete ptr; + ptr = NULL; + } + return &ptr; + } + + T** use() { + return &ptr; + } +}; + +template inline +void swap(scoped_ptr& a, scoped_ptr& b) { + a.swap(b); +} + + + + +// scoped_array extends scoped_ptr to arrays. Deletion of the array pointed to +// is guaranteed, either on destruction of the scoped_array or via an explicit +// reset(). Use shared_array or std::vector if your needs are more complex. + +template +class scoped_array { + private: + + T* ptr; + + scoped_array(scoped_array const &); + scoped_array & operator=(scoped_array const &); + + public: + + typedef T element_type; + + explicit scoped_array(T* p = NULL) : ptr(p) {} + + ~scoped_array() { + typedef char type_must_be_complete[sizeof(T)]; + delete[] ptr; + } + + void reset(T* p = NULL) { + typedef char type_must_be_complete[sizeof(T)]; + + if (ptr != p) { + T* arr = ptr; + ptr = p; + // Delete last, in case arr destructor indirectly results in ~scoped_array + delete [] arr; + } + } + + T& operator[](ptrdiff_t i) const { + assert(ptr != NULL); + assert(i >= 0); + return ptr[i]; + } + + T* get() const { + return ptr; + } + + void swap(scoped_array & b) { + T* tmp = b.ptr; + b.ptr = ptr; + ptr = tmp; + } + + T* release() { + T* tmp = ptr; + ptr = NULL; + return tmp; + } + + T** accept() { + if (ptr) { + delete [] ptr; + ptr = NULL; + } + return &ptr; + } +}; + +template inline +void swap(scoped_array& a, scoped_array& b) { + a.swap(b); +} + +// scoped_ptr_malloc<> is similar to scoped_ptr<>, but it accepts a +// second template argument, the function used to free the object. + +template class scoped_ptr_malloc { + private: + + T* ptr; + + scoped_ptr_malloc(scoped_ptr_malloc const &); + scoped_ptr_malloc & operator=(scoped_ptr_malloc const &); + + public: + + typedef T element_type; + + explicit scoped_ptr_malloc(T* p = 0): ptr(p) {} + + ~scoped_ptr_malloc() { + FF(static_cast(ptr)); + } + + void reset(T* p = 0) { + if (ptr != p) { + FF(static_cast(ptr)); + ptr = p; + } + } + + T& operator*() const { + assert(ptr != 0); + return *ptr; + } + + T* operator->() const { + assert(ptr != 0); + return ptr; + } + + T* get() const { + return ptr; + } + + void swap(scoped_ptr_malloc & b) { + T* tmp = b.ptr; + b.ptr = ptr; + ptr = tmp; + } + + T* release() { + T* tmp = ptr; + ptr = 0; + return tmp; + } + + T** accept() { + if (ptr) { + FF(static_cast(ptr)); + ptr = 0; + } + return &ptr; + } +}; + +template inline +void swap(scoped_ptr_malloc& a, scoped_ptr_malloc& b) { + a.swap(b); +} + +} // namespace AgoraRTC + +namespace AgoraAPM { + template > + class scoped_ptr { + + // TODO(ajm): If we ever import RefCountedBase, this check needs to be + // enabled. + //static_assert(rtc::internal::IsNotRefCounted::value, + // "T is refcounted type and needs scoped refptr"); + + public: + // The element and deleter types. + typedef T element_type; + typedef D deleter_type; + + // Constructor. Takes ownership of p. + explicit scoped_ptr(element_type* p=NULL) : impl_(p) {} + + // Constructor. Allows initialization of a stateful deleter. + scoped_ptr(element_type* p, const D& d) : impl_(p, d) {} + + // Constructor. Allows construction from a scoped_ptr rvalue for a + // convertible type and deleter. + // + // IMPLEMENTATION NOTE: C++11 unique_ptr<> keeps this constructor distinct + // from the normal move constructor. By C++11 20.7.1.2.1.21, this constructor + // has different post-conditions if D is a reference type. Since this + // implementation does not support deleters with reference type, + // we do not need a separate move constructor allowing us to avoid one + // use of SFINAE. You only need to care about this if you modify the + // implementation of scoped_ptr. +// template +// scoped_ptr(scoped_ptr&& other) +// : impl_(&other.impl_) { +// // static_assert(!AgoraRTC::is_array::value, "U cannot be an array"); +// } +// +// // operator=. Allows assignment from a scoped_ptr rvalue for a convertible +// // type and deleter. +// // +// // IMPLEMENTATION NOTE: C++11 unique_ptr<> keeps this operator= distinct from +// // the normal move assignment operator. By C++11 20.7.1.2.3.4, this templated +// // form has different requirements on for move-only Deleters. Since this +// // implementation does not support move-only Deleters, we do not need a +// // separate move assignment operator allowing us to avoid one use of SFINAE. +// // You only need to care about this if you modify the implementation of +// // scoped_ptr. +// template +// scoped_ptr& operator=(scoped_ptr&& rhs) { +// // static_assert(!AgoraRTC::is_array::value, "U cannot be an array"); +// impl_.TakeState(&rhs.impl_); +// return *this; +// } + + // Deleted copy constructor and copy assignment, to make the type move-only. + private: + scoped_ptr(const scoped_ptr& other); + scoped_ptr& operator=(const scoped_ptr& other); + public: + // Reset. Deletes the currently owned object, if any. + // Then takes ownership of a new object, if given. + void reset(element_type* p = NULL) { impl_.reset(p); } + + // Accessors to get the owned object. + // operator* and operator-> will assert() if there is no current object. + element_type& operator*() const { + assert(impl_.get() != NULL); + return *impl_.get(); + } + element_type* operator->() const { + assert(impl_.get() != NULL); + return impl_.get(); + } + element_type* get() const { return impl_.get(); } + + // Access to the deleter. + deleter_type& get_deleter() { return impl_.get_deleter(); } + const deleter_type& get_deleter() const { return impl_.get_deleter(); } + + // Allow scoped_ptr to be used in boolean expressions, but not + // implicitly convertible to a real bool (which is dangerous). + // + // Note that this trick is only safe when the == and != operators + // are declared explicitly, as otherwise "scoped_ptr1 == + // scoped_ptr2" will compile but do the wrong thing (i.e., convert + // to Testable and then do the comparison). + private: + typedef AgoraRTC::internal::scoped_ptr_impl + scoped_ptr::*Testable; + + public: + operator Testable() const { + return impl_.get() ? &scoped_ptr::impl_ : NULL; + } + + // Comparison operators. + // These return whether two scoped_ptr refer to the same object, not just to + // two different but equal objects. + bool operator==(const element_type* p) const { return impl_.get() == p; } + bool operator!=(const element_type* p) const { return impl_.get() != p; } + + // Swap two scoped pointers. + void swap(scoped_ptr& p2) { + impl_.swap(p2.impl_); + } + + // Release a pointer. + // The return value is the current pointer held by this object. If this object + // holds a NULL, the return value is NULL. After this operation, this + // object will hold a NULL, and will not own the object any more. + element_type* release() { + return impl_.release(); + } + + // Delete the currently held pointer and return a pointer + // to allow overwriting of the current pointer address. + element_type** accept() { + return impl_.accept(); + } + + // Return a pointer to the current pointer address. + element_type** use(){ + return impl_.use(); + } + + private: + // Needed to reach into |impl_| in the constructor. + template friend class scoped_ptr; + AgoraRTC::internal::scoped_ptr_impl impl_; + + // Forbidden for API compatibility with std::unique_ptr. + explicit scoped_ptr(int disallow_construction_from_null); + + // Forbid comparison of scoped_ptr types. If U != T, it totally + // doesn't make sense, and if U == T, it still doesn't make sense + // because you should never have the same object owned by two different + // scoped_ptrs. + template bool operator==(scoped_ptr const& p2) const; + template bool operator!=(scoped_ptr const& p2) const; + }; + + template + class scoped_ptr { + public: + // The element and deleter types. + typedef T element_type; + typedef D deleter_type; + + // Constructor. Stores the given array. Note that the argument's type + // must exactly match T*. In particular: + // - it cannot be a pointer to a type derived from T, because it is + // inherently unsafe in the general case to access an array through a + // pointer whose dynamic type does not match its static type (eg., if + // T and the derived types had different sizes access would be + // incorrectly calculated). Deletion is also always undefined + // (C++98 [expr.delete]p3). If you're doing this, fix your code. + // - it cannot be const-qualified differently from T per unique_ptr spec + // (http://cplusplus.github.com/LWG/lwg-active.html#2118). Users wanting + // to work around this may use implicit_cast(). + // However, because of the first bullet in this comment, users MUST + // NOT use implicit_cast() to upcast the static type of the array. + explicit scoped_ptr(element_type* array=NULL) : impl_(array) {} + + // operator=. Allows assignment from a NULL. Deletes the currently owned + // array, if any. + scoped_ptr& operator=(element_type *t) { + reset(t); + return *this; + } + private: + // Deleted copy constructor and copy assignment, to make the type move-only. + scoped_ptr(const scoped_ptr& other); + scoped_ptr& operator=(const scoped_ptr& other); + public: + // Reset. Deletes the currently owned array, if any. + // Then takes ownership of a new object, if given. + void reset(element_type* array = NULL) { impl_.reset(array); } + + // Accessors to get the owned array. + element_type& operator[](size_t i) const { + assert(impl_.get() != NULL); + return impl_.get()[i]; + } + element_type* get() const { return impl_.get(); } + + // Access to the deleter. + deleter_type& get_deleter() { return impl_.get_deleter(); } + const deleter_type& get_deleter() const { return impl_.get_deleter(); } + + // Allow scoped_ptr to be used in boolean expressions, but not + // implicitly convertible to a real bool (which is dangerous). + private: + typedef AgoraRTC::internal::scoped_ptr_impl + scoped_ptr::*Testable; + + public: + operator Testable() const { + return impl_.get() ? &scoped_ptr::impl_ : NULL; + } + + // Comparison operators. + // These return whether two scoped_ptr refer to the same object, not just to + // two different but equal objects. + bool operator==(element_type* array) const { return impl_.get() == array; } + bool operator!=(element_type* array) const { return impl_.get() != array; } + + // Swap two scoped pointers. + void swap(scoped_ptr& p2) { + impl_.swap(p2.impl_); + } + + // Release a pointer. + // The return value is the current pointer held by this object. If this object + // holds a NULL, the return value is NULL. After this operation, this + // object will hold a NULL, and will not own the object any more. + element_type* release() { + return impl_.release(); + } + + // Delete the currently held pointer and return a pointer + // to allow overwriting of the current pointer address. + element_type** accept() { + return impl_.accept(); +} + +// Return a pointer to the current pointer address. +element_type** use(){ + return impl_.use(); +} + +private: + // Force element_type to be a complete type. + enum { type_must_be_complete = sizeof(element_type) }; + + // Actually hold the data. + AgoraRTC::internal::scoped_ptr_impl impl_; + + // Disable initialization from any type other than element_type*, by + // providing a constructor that matches such an initialization, but is + // private and has no definition. This is disabled because it is not safe to + // call delete[] on an array whose static type does not match its dynamic + // type. + template explicit scoped_ptr(U* array); + explicit scoped_ptr(int disallow_construction_from_null); + + // Disable reset() from any type other than element_type*, for the same + // reasons as the constructor above. + template void reset(U* array); + void reset(int disallow_reset_from_null); + + // Forbid comparison of scoped_ptr types. If U != T, it totally + // doesn't make sense, and if U == T, it still doesn't make sense + // because you should never have the same object owned by two different + // scoped_ptrs. + template bool operator==(scoped_ptr const& p2) const; + template bool operator!=(scoped_ptr const& p2) const; +}; +} + +#endif // #ifndef WEBRTC_SYSTEM_WRAPPERS_INTERFACE_SCOPED_PTR_H_ diff --git a/iOS/APIExample/Common/RtcChannelPublishPlugin/utils/template_util.h b/iOS/APIExample/Common/RtcChannelPublishPlugin/utils/template_util.h new file mode 100755 index 000000000..3c347cde5 --- /dev/null +++ b/iOS/APIExample/Common/RtcChannelPublishPlugin/utils/template_util.h @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +// Borrowed from Chromium's src/base/template_util.h. + +#ifndef WEBRTC_BASE_TEMPLATE_UTIL_H_ +#define WEBRTC_BASE_TEMPLATE_UTIL_H_ + +#include // For size_t. + +namespace AgoraRTC { + +// Template definitions from tr1. + +template +struct integral_constant { + static const T value = v; + typedef T value_type; + typedef integral_constant type; +}; + +template const T integral_constant::value; + +typedef integral_constant true_type; +typedef integral_constant false_type; + +template struct is_pointer : false_type {}; +template struct is_pointer : true_type {}; + +template struct is_same : public false_type {}; +template struct is_same : true_type {}; + +template struct is_array : public false_type {}; +template struct is_array : public true_type {}; +template struct is_array : public true_type {}; + +template struct is_non_const_reference : false_type {}; +template struct is_non_const_reference : true_type {}; +template struct is_non_const_reference : false_type {}; + +template struct is_void : false_type {}; +template <> struct is_void : true_type {}; + +namespace internal { + +// Types YesType and NoType are guaranteed such that sizeof(YesType) < +// sizeof(NoType). +typedef char YesType; + +struct NoType { + YesType dummy[2]; +}; + +// This class is an implementation detail for is_convertible, and you +// don't need to know how it works to use is_convertible. For those +// who care: we declare two different functions, one whose argument is +// of type To and one with a variadic argument list. We give them +// return types of different size, so we can use sizeof to trick the +// compiler into telling us which function it would have chosen if we +// had called it with an argument of type From. See Alexandrescu's +// _Modern C++ Design_ for more details on this sort of trick. + +struct ConvertHelper { + template + static YesType Test(To); + + template + static NoType Test(...); + + template + static From& Create(); +}; + +// Used to determine if a type is a struct/union/class. Inspired by Boost's +// is_class type_trait implementation. +struct IsClassHelper { + template + static YesType Test(void(C::*)(void)); + + template + static NoType Test(...); +}; + +} // namespace internal + +// Inherits from true_type if From is convertible to To, false_type otherwise. +// +// Note that if the type is convertible, this will be a true_type REGARDLESS +// of whether or not the conversion would emit a warning. +template +struct is_convertible + : integral_constant( + internal::ConvertHelper::Create())) == + sizeof(internal::YesType)> { +}; + +template +struct is_class + : integral_constant(0)) == + sizeof(internal::YesType)> { +}; + +} // namespace AgoraRTC + +#endif // WEBRTC_BASE_TEMPLATE_UTIL_H_ diff --git a/iOS/APIExample/Common/Settings/SettingsCells.swift b/iOS/APIExample/Common/Settings/SettingsCells.swift new file mode 100644 index 000000000..871b95b7f --- /dev/null +++ b/iOS/APIExample/Common/Settings/SettingsCells.swift @@ -0,0 +1,136 @@ +// +// SettingsCells.swift +// APIExample +// +// Created by ZQZ on 2020/11/28. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import Foundation + +class SettingsBaseCell : UITableViewCell +{ + var configs:SettingsBaseParam? + weak var delegate:SettingsViewControllerDelegate? + func configure(configs:SettingsBaseParam){ + self.configs = configs + } +} + +class SettingsBaseParam: NSObject +{ + var key:String + var label:String + var type:String + + init(key:String, label:String, type:String) { + self.key = key + self.label = label + self.type = type + } +} + +class SettingsSliderCell : SettingsBaseCell +{ + @IBOutlet weak var settingLabel: UILabel! + @IBOutlet weak var settingSlider: UISlider! + @IBOutlet weak var settingValue: UILabel! + + @IBAction func onSliderValueChanged(sender:UISlider){ + let val = (sender.value*100).rounded()/100 + settingValue.text = "\(val)" + guard let configs = self.configs as? SettingsSliderParam else {return} + delegate?.didChangeValue(type: "SettingsSliderCell", key: configs.key, value: val) + } + + override func configure(configs: SettingsBaseParam) { + super.configure(configs: configs) + + guard let param = configs as? SettingsSliderParam else {return} + settingLabel.text = param.label + settingSlider.value = param.value + settingSlider.minimumValue = param.minimumValue + settingSlider.maximumValue = param.maximumValue + settingValue.text = "\(settingSlider.value)" + } +} + +class SettingsSliderParam: SettingsBaseParam { + var value:Float + var minimumValue:Float + var maximumValue:Float + init(key:String, label:String, value:Float, minimumValue:Float, maximumValue:Float) { + self.value = value + self.minimumValue = minimumValue + self.maximumValue = maximumValue + super.init(key: key, label: label, type: "SliderCell") + } +} + + +class SettingsLabelCell : SettingsBaseCell +{ + @IBOutlet weak var settingLabel: UILabel! + @IBOutlet weak var settingValue: UILabel! + + override func configure(configs: SettingsBaseParam) { + super.configure(configs: configs) + + guard let param = configs as? SettingsLabelParam else {return} + settingLabel.text = param.label + settingValue.text = param.value + } +} + +class SettingsLabelParam: SettingsBaseParam { + var value:String + init(key:String, label:String, value:String) { + self.value = value + super.init(key: key, label: label, type: "LabelCell") + } +} + +class SettingsSelectCell : SettingsBaseCell +{ + @IBOutlet weak var settingLabel: UILabel! + @IBOutlet weak var settingBtn: UIButton! + + override func configure(configs: SettingsBaseParam) { + super.configure(configs: configs) + + guard let param = configs as? SettingsSelectParam else {return} + settingLabel.text = param.label + settingBtn.setTitle(param.value, for: .normal) + } + + func getSelectAction(_ option:SettingItemOption) -> UIAlertAction{ + return UIAlertAction(title: "\(option.label)", style: .default, handler: {[unowned self] action in + guard let param = self.configs as? SettingsSelectParam else {return} + self.settingBtn.setTitle(option.label, for: .normal) + param.settingItem.selected = option.idx + self.delegate?.didChangeValue(type: "SettingsSelectCell", key: param.key, value: param.settingItem) + }) + } + + @IBAction func onSelect(_ sender:UIButton) { + let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + guard let param = configs as? SettingsSelectParam else {return} + for option in param.settingItem.options { + alert.addAction(getSelectAction(option)) + } + alert.addCancelAction() + param.context?.present(alert, animated: true, completion: nil) + } +} + +class SettingsSelectParam: SettingsBaseParam { + var value:String + var settingItem:SettingItem + weak var context:UIViewController?; + init(key:String, label:String, settingItem:SettingItem, context:UIViewController) { + self.settingItem = settingItem + self.context = context + self.value = settingItem.selectedOption().label + super.init(key: key, label: label, type: "SelectCell") + } +} diff --git a/iOS/APIExample/Common/Settings/SettingsViewController.swift b/iOS/APIExample/Common/Settings/SettingsViewController.swift new file mode 100644 index 000000000..5bcb229ae --- /dev/null +++ b/iOS/APIExample/Common/Settings/SettingsViewController.swift @@ -0,0 +1,42 @@ +// +// SettingsViewController.swift +// APIExample +// +// Created by 张乾泽 on 2020/7/26. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import Foundation +import UIKit + +protocol SettingsViewControllerDelegate: AnyObject { + func didChangeValue(type:String, key:String, value: Any) +} + +class SettingsViewController : UITableViewController +{ + var sections:[[SettingsBaseParam]] = [] + var sectionNames:[String] = [] + weak var settingsDelegate:SettingsViewControllerDelegate? + override func numberOfSections(in tableView: UITableView) -> Int { + return sections.count + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + let params = sections[section] + return params.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let param = sections[indexPath.section][indexPath.row] + guard let cell = tableView.dequeueReusableCell(withIdentifier: param.type, for: indexPath) as? SettingsBaseCell else {return UITableViewCell()} + cell.delegate = settingsDelegate + cell.configure(configs: param) + + return cell + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return sectionNames[section] + } +} diff --git a/iOS/APIExample/Common/StatisticsInfo.swift b/iOS/APIExample/Common/StatisticsInfo.swift index 3871ae5cb..6da7f74ab 100755 --- a/iOS/APIExample/Common/StatisticsInfo.swift +++ b/iOS/APIExample/Common/StatisticsInfo.swift @@ -11,7 +11,9 @@ import AgoraRtcKit struct StatisticsInfo { struct LocalInfo { - var stats = AgoraChannelStats() + var channelStats = AgoraChannelStats() + var videoStats = AgoraRtcLocalVideoStats() + var audioStats = AgoraRtcLocalAudioStats() } struct RemoteInfo { @@ -31,10 +33,7 @@ struct StatisticsInfo { } var dimension = CGSize.zero - var fps = 0 - - var txQuality: AgoraNetworkQuality = .unknown - var rxQuality: AgoraNetworkQuality = .unknown + var fps:UInt = 0 var type: StatisticsType @@ -46,8 +45,44 @@ struct StatisticsInfo { guard self.type.isLocal else { return } - let info = LocalInfo(stats: stats) - self.type = .local(info) + switch type { + case .local(let info): + var new = info + new.channelStats = stats + self.type = .local(new) + default: + break + } + } + + mutating func updateLocalVideoStats(_ stats: AgoraRtcLocalVideoStats) { + guard self.type.isLocal else { + return + } + switch type { + case .local(let info): + var new = info + new.videoStats = stats + self.type = .local(new) + default: + break + } + dimension = CGSize(width: Int(stats.encodedFrameWidth), height: Int(stats.encodedFrameHeight)) + fps = stats.sentFrameRate + } + + mutating func updateLocalAudioStats(_ stats: AgoraRtcLocalAudioStats) { + guard self.type.isLocal else { + return + } + switch type { + case .local(let info): + var new = info + new.audioStats = stats + self.type = .local(new) + default: + break + } } mutating func updateVideoStats(_ stats: AgoraRtcRemoteVideoStats) { @@ -55,6 +90,8 @@ struct StatisticsInfo { case .remote(let info): var new = info new.videoStats = stats + dimension = CGSize(width: Int(stats.width), height: Int(stats.height)) + fps = stats.rendererOutputFrameRate self.type = .remote(new) default: break @@ -72,35 +109,34 @@ struct StatisticsInfo { } } - func description() -> String { + func description(audioOnly:Bool) -> String { var full: String switch type { - case .local(let info): full = localDescription(info: info) - case .remote(let info): full = remoteDescription(info: info) + case .local(let info): full = localDescription(info: info, audioOnly: audioOnly) + case .remote(let info): full = remoteDescription(info: info, audioOnly: audioOnly) } return full } - func localDescription(info: LocalInfo) -> String { - let join = "\n" + func localDescription(info: LocalInfo, audioOnly: Bool) -> String { - let dimensionFps = "\(Int(dimension.width))×\(Int(dimension.height)), \(fps)fps" - let quality = "Send/Recv Quality: \(txQuality.description())/\(rxQuality.description())" + let dimensionFps = "\(Int(dimension.width))×\(Int(dimension.height)),\(fps)fps" - let lastmile = "Lastmile Delay: \(info.stats.lastmileDelay)ms" - let videoSendRecv = "Video Send/Recv: \(info.stats.txVideoKBitrate)kbps/\(info.stats.rxVideoKBitrate)kbps" - let audioSendRecv = "Audio Send/Recv: \(info.stats.txAudioKBitrate)kbps/\(info.stats.rxAudioKBitrate)kbps" + let lastmile = "LM Delay: \(info.channelStats.lastmileDelay)ms" + let videoSend = "VSend: \(info.videoStats.sentBitrate)kbps" + let audioSend = "ASend: \(info.audioStats.sentBitrate)kbps" + let cpu = "CPU: \(info.channelStats.cpuAppUsage)%/\(info.channelStats.cpuTotalUsage)%" + let vSendLoss = "VSend Loss: \(info.videoStats.txPacketLossRate)%" + let aSendLoss = "ASend Loss: \(info.audioStats.txPacketLossRate)%" - let cpu = "CPU: App/Total \(info.stats.cpuAppUsage)%/\(info.stats.cpuTotalUsage)%" - let sendRecvLoss = "Send/Recv Loss: \(info.stats.txPacketLossRate)%/\(info.stats.rxPacketLossRate)%" - return dimensionFps + join + lastmile + join + videoSendRecv + join + audioSendRecv + join + cpu + join + quality + join + sendRecvLoss + if(audioOnly) { + return [lastmile,audioSend,cpu,aSendLoss].joined(separator: "\n") + } + return [dimensionFps,lastmile,videoSend,audioSend,cpu,vSendLoss,aSendLoss].joined(separator: "\n") } - func remoteDescription(info: RemoteInfo) -> String { - let join = "\n" - - let dimensionFpsBit = "\(Int(dimension.width))×\(Int(dimension.height)), \(fps)fps, \(info.videoStats.receivedBitrate)kbps" - let quality = "Send/Recv Quality: \(txQuality.description())/\(rxQuality.description())" + func remoteDescription(info: RemoteInfo, audioOnly: Bool) -> String { + let dimensionFpsBit = "\(Int(dimension.width))×\(Int(dimension.height)), \(fps)fps" var audioQuality: AgoraNetworkQuality if let quality = AgoraNetworkQuality(rawValue: info.audioStats.quality) { @@ -109,9 +145,15 @@ struct StatisticsInfo { audioQuality = AgoraNetworkQuality.unknown } - let audioNet = "Audio Net Delay/Jitter: \(info.audioStats.networkTransportDelay)ms/\(info.audioStats.jitterBufferDelay)ms)" - let audioLoss = "Audio Loss/Quality: \(info.audioStats.audioLossRate)% \(audioQuality.description())" + let videoRecv = "VRecv: \(info.videoStats.receivedBitrate)kbps" + let audioRecv = "ARecv: \(info.audioStats.receivedBitrate)kbps" - return dimensionFpsBit + join + quality + join + audioNet + join + audioLoss + let videoLoss = "VLoss: \(info.videoStats.packetLossRate)%" + let audioLoss = "ALoss: \(info.audioStats.audioLossRate)%" + let aquality = "AQuality: \(audioQuality.description())" + if(audioOnly) { + return [audioRecv,audioLoss,aquality].joined(separator: "\n") + } + return [dimensionFpsBit,videoRecv,audioRecv,videoLoss,audioLoss,aquality].joined(separator: "\n") } } diff --git a/iOS/APIExample/Common/UITypeAlias.swift b/iOS/APIExample/Common/UITypeAlias.swift new file mode 100644 index 000000000..4104d619a --- /dev/null +++ b/iOS/APIExample/Common/UITypeAlias.swift @@ -0,0 +1,887 @@ +// +// UITypeAlias.swift +// APIExample +// +// Created by CavanSu on 2020/5/26. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +#if os(iOS) +import UIKit +#else +import Cocoa +#endif + +typealias Color = UIColor + +typealias MainFont = Font.HelveticaNeue + +extension String { + var localized: String { NSLocalizedString(self, comment: "") } +} + +enum Font { + enum HelveticaNeue: String { + case ultraLightItalic = "UltraLightItalic" + case medium = "Medium" + case mediumItalic = "MediumItalic" + case ultraLight = "UltraLight" + case italic = "Italic" + case light = "Light" + case thinItalic = "ThinItalic" + case lightItalic = "LightItalic" + case bold = "Bold" + case thin = "Thin" + case condensedBlack = "CondensedBlack" + case condensedBold = "CondensedBold" + case boldItalic = "BoldItalic" + + func with(size: CGFloat) -> UIFont { + return UIFont(name: "HelveticaNeue-\(rawValue)", size: size)! + } + } +} + +extension UIColor { + + /// Get color rgba components in order. + func rgba() -> (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) { + let components = self.cgColor.components + let numberOfComponents = self.cgColor.numberOfComponents + + switch numberOfComponents { + case 4: + return (components![0], components![1], components![2], components![3]) + case 2: + return (components![0], components![0], components![0], components![1]) + default: + // FIXME: Fallback to black + return (0, 0, 0, 1) + } + } + + /// Check the black or white contrast on given color. + func blackOrWhiteContrastingColor() -> Color { + let rgbaT = rgba() + let value = 1 - ((0.299 * rgbaT.r) + (0.587 * rgbaT.g) + (0.114 * rgbaT.b)); + return value < 0.5 ? Color.black : Color.white + } + +} + +enum AssetsColor : String { + case videoBackground + case videoPlaceholder + case textShadow + case btnPanelBackground +} + +extension UIColor { + static func appColor(_ name: AssetsColor) -> UIColor? { + return UIColor(named: name.rawValue) + } +} + +extension UIView { + /// Adds constraints to this `UIView` instances `superview` object to make sure this always has the same size as the superview. + /// Please note that this has no effect if its `superview` is `nil` – add this `UIView` instance as a subview before calling this. + func bindFrameToSuperviewBounds() { + guard let superview = self.superview else { + print("Error! `superview` was nil – call `addSubview(view: UIView)` before calling `bindFrameToSuperviewBounds()` to fix this.") + return + } + + self.translatesAutoresizingMaskIntoConstraints = false + self.topAnchor.constraint(equalTo: superview.topAnchor, constant: 0).isActive = true + self.bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: 0).isActive = true + self.leadingAnchor.constraint(equalTo: superview.leadingAnchor, constant: 0).isActive = true + self.trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: 0).isActive = true + + } +} + +//MARK: - Color +#if os(iOS) +typealias AGColor = UIColor +#else +typealias AGColor = NSColor +#endif +extension AGColor { + convenience init(hex: Int, alpha: CGFloat = 1) { + func transform(_ input: Int, offset: Int = 0) -> CGFloat { + let value = (input >> offset) & 0xff + return CGFloat(value) / 255 + } + + self.init(red: transform(hex, offset: 16), + green: transform(hex, offset: 8), + blue: transform(hex), + alpha: alpha) + } + + func rgbValue() -> (red: CGFloat, green: CGFloat, blue: CGFloat) { + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + + getRed(&red, green: &green, blue: &blue, alpha: nil) + + return (red * 255, green * 255, blue * 255) + } + + convenience init(hex: String, alpha: CGFloat = 1) { + var cString: String = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() + + if (cString.hasPrefix("#")) { + let range = cString.index(after: cString.startIndex) ..< cString.endIndex + cString = String(cString[range]) + } + if (cString.hasPrefix("0X")) { + let range = cString.index(cString.startIndex, offsetBy: 2) ..< cString.endIndex + cString = String(cString[range]) + } + + + if (cString.count != 6) { + self.init() + return + } + + let scanner = Scanner(string: cString) + var hexValue: UInt64 = 0 + scanner.scanHexInt64(&hexValue) + self.init(hex: Int(hexValue), alpha: alpha) + } + + static func randomColor() -> AGColor { + let randomHex = Int(arc4random_uniform(0xCCCCCC) + 0x555555) + return AGColor(hex: randomHex) + } +} + +//MARK: - Font +#if os(iOS) +typealias AGFont = UIFont +#else +typealias AGFont = NSFont +#endif + +//MARK: - Image +#if os(iOS) +typealias AGImage = UIImage +#else +typealias AGImage = NSImage +#endif + +// MARK: - Label +#if os(iOS) +typealias AGLabel = UILabel +#else +typealias AGLabel = NSTextField +#endif +extension AGLabel { + var formattedFloatValue: Float { + get { + #if os(iOS) + if let text = text, let value = Double(text) { + return Float(value) + } else { + return 0 + } + #else + return floatValue + #endif + } + set { + #if os(iOS) + text = NSString(format: "%.1f", newValue) as String + #else + stringValue = NSString(format: "%.1f", newValue) as String + #endif + } + } + + var formattedCGFloatValue: CGFloat { + get { + #if os(iOS) + if let text = text, let value = Double(text) { + return CGFloat(value) + } else { + return 0 + } + #else + return CGFloat(floatValue) + #endif + } + set { + #if os(iOS) + text = NSString(format: "%.1f", newValue) as String + #else + stringValue = NSString(format: "%.1f", newValue) as String + #endif + } + } + + var formattedIntValue: Int { + get { + #if os(iOS) + if let text = text, let value = Int(text) { + return value + } else { + return 0 + } + #else + return integerValue + #endif + } + set { + #if os(iOS) + text = "\(newValue)" + #else + stringValue = "\(newValue)" + #endif + } + } + + #if os(macOS) + var text: String? { + get { + return stringValue + } + set { + if let newValue = newValue { + stringValue = newValue + } + } + } + #endif +} + +//MARK: - TextField +#if os(iOS) +typealias AGTextField = UITextField +#else +typealias AGTextField = NSTextField +#endif + +extension AGTextField { + #if os(iOS) + var integerValue: Int { + get { + if let text = text, let value = Int(text) { + return value + } else { + return 0 + } + } + set { + text = "\(newValue)" + } + } + + var formattedIntValue: Int { + get { + return integerValue + } + set { + integerValue = newValue + } + } + + var cgFloatValue: CGFloat { + get { + if let text = text, let value = Double(text) { + return CGFloat(value) + } else { + return 0 + } + } + set { + text = "\(newValue)" + } + } + + var formattedCGFloatValue: CGFloat { + get { + return CGFloat(cgFloatValue) + } + set { + cgFloatValue = newValue + } + } + + var formattedFloatValue: Float { + get { + if let text = text, let value = Double(text) { + return Float(value) + } else { + return 0 + } + } + set { + text = NSString(format: "%.1f", newValue) as String + } + } + + var stringValue: String { + get { + return text! + } + set { + text = newValue + } + } + #endif + var placeholderAGString: String? { + get { + #if os(iOS) + return placeholder + #else + return placeholderString + #endif + } + set { + #if os(iOS) + placeholder = newValue + #else + placeholderString = placeholderAGString + #endif + } + } +} + +//MARK: - Indicator +#if os(iOS) +typealias AGIndicator = UIActivityIndicatorView +#else +typealias AGIndicator = NSProgressIndicator +#endif + +extension AGIndicator { + + func startAnimation() { + #if os(iOS) + self.startAnimating() + #else + self.startAnimation(nil) + #endif + } + + func stopAnimation() { + #if os(iOS) + self.stopAnimating() + #else + self.stopAnimation(nil) + #endif + } + +} + +//MARK: - View +#if os(iOS) +typealias AGView = UIView +#else +typealias AGView = NSView +#endif +extension AGView { + var cornerRadius: CGFloat? { + get { + #if os(iOS) + return layer.cornerRadius + #else + return layer?.cornerRadius + #endif + } + set { + guard let newValue = newValue else { + return + } + #if os(iOS) + layer.cornerRadius = newValue + #else + wantsLayer = true + layer?.cornerRadius = newValue + #endif + } + } + + var masksToBounds: Bool? { + get { + #if os(iOS) + return layer.masksToBounds + #else + return layer?.masksToBounds + #endif + } + set { + guard let newValue = newValue else { + return + } + #if os(iOS) + layer.masksToBounds = newValue + #else + wantsLayer = true + layer?.masksToBounds = newValue + #endif + } + } + + var borderWidth: CGFloat { + get { + #if os(iOS) + return layer.borderWidth + #else + guard let borderWidth = layer?.borderWidth else { + return 0 + } + return borderWidth + #endif + } + set { + #if os(iOS) + layer.borderWidth = newValue + #else + wantsLayer = true + layer?.borderWidth = newValue + #endif + } + } + + var borderColor: CGColor { + get { + #if os(iOS) + guard let borderColor = layer.borderColor else { + return AGColor.clear.cgColor + } + return borderColor + #else + guard let borderColor = layer?.borderColor else { + return AGColor.clear.cgColor + } + return borderColor + #endif + } + set { + #if os(iOS) + layer.borderColor = newValue + #else + wantsLayer = true + layer?.borderColor = newValue + #endif + } + } + + #if os(macOS) + var backgroundColor: AGColor? { + get { + if let cgColor = layer?.backgroundColor { + return AGColor(cgColor: cgColor) + } else { + return nil + } + } + set { + if let newValue = newValue { + wantsLayer = true + layer?.backgroundColor = newValue.cgColor + } + } + } + + var center: CGPoint { + get { + return CGPoint(x: self.frame.width / 2, y: self.frame.height / 2) + } + set { + self.frame.origin = CGPoint(x: newValue.x - self.frame.width / 2, y: newValue.y - self.frame.height / 2) + } + } + #endif +} + + +#if os(iOS) +typealias AGVisualEffectView = UIVisualEffectView +#else +typealias AGVisualEffectView = NSVisualEffectView +#endif + +//MARK: - ImageView +#if os(iOS) +typealias AGImageView = UIImageView +#else +typealias AGImageView = NSImageView +#endif + +//MARK: - TableView +#if os(iOS) +typealias AGTableView = UITableView +#else +typealias AGTableView = NSTableView +#endif + +//MARK: - TableViewCell +#if os(iOS) +typealias AGTableViewCell = UITableViewCell +#else +typealias AGTableViewCell = NSTableCellView +#endif + +//MARK: - CollectionView +#if os(iOS) +typealias AGCollectionView = UICollectionView +#else +typealias AGCollectionView = NSCollectionView +#endif + +#if os(iOS) +typealias AGCollectionViewFlowLayout = UICollectionViewFlowLayout +#else +typealias AGCollectionViewFlowLayout = NSCollectionViewFlowLayout +#endif + +//MARK: - CollectionViewCell +#if os(iOS) +typealias AGCollectionViewCell = UICollectionViewCell +#else +typealias AGCollectionViewCell = NSCollectionViewItem +#endif + +extension AGCollectionViewCell { + #if os(OSX) + var contentView: AGView { + get { + return view + } + set { + view = newValue + } + } + #endif +} + +//MARK: - Button +#if os(iOS) +typealias AGButton = UIButton +#else +typealias AGButton = NSButton +#endif +extension AGButton { + #if os(iOS) + var image: AGImage? { + get { + return image(for: .normal) + } + set { + setImage(newValue, for: .normal) + } + } + var highlightImage: AGImage? { + get { + return image(for: .highlighted) + } + set { + setImage(newValue, for: .highlighted) + } + } + var title: String? { + get { + return title(for: .normal) + } + set { + setTitle(newValue, for: .normal) + } + } + + #else + var textColor: AGColor { + get { + return AGColor.black + } + set { + let pstyle = NSMutableParagraphStyle() + pstyle.alignment = .left + attributedTitle = NSAttributedString(string: title, attributes: [ NSAttributedString.Key.foregroundColor : newValue, NSAttributedString.Key.paragraphStyle : pstyle ]) + } + } + #endif + + func switchImage(toImage: AGImage) { + #if os(iOS) + UIView.animate(withDuration: 0.15, animations: { + self.isEnabled = false + self.alpha = 0.3 + }) { (_) in + self.image = toImage + self.alpha = 1.0 + self.isEnabled = true + } + #else + NSAnimationContext.runAnimationGroup({ (context) in + context.duration = 0.3 + self.isEnabled = false + self.animator().alphaValue = 0.3 + }) { + self.image = toImage + self.alphaValue = 1.0 + self.isEnabled = true + } + #endif + } +} + +//MARK: - Switch +#if os(iOS) +typealias AGSwitch = UISwitch +#else +typealias AGSwitch = NSButton +#endif +#if os(macOS) +extension AGSwitch { + var isOn: Bool { + get { + return state != .off + } + set { + state = newValue ? .on : .off + } + } +} +#endif + +//MARK: - WebView +#if os(iOS) +typealias AGWebView = UIWebView +#else +import WebKit +typealias AGWebView = WebView +#endif + +#if os(macOS) +extension AGWebView { + func loadRequest(_ request: URLRequest) { + self.mainFrame.load(request) + } +} +#endif + +//MARK: - Slider +#if os(iOS) +typealias AGSlider = UISlider +#else +typealias AGSlider = NSSlider +#endif +extension AGSlider { + #if os(iOS) + var floatValue: Float { + get { + return value + } + set { + setValue(newValue, animated: false) + } + } + var cgFloatValue: CGFloat { + get { + return CGFloat(value) + } + set { + setValue(Float(newValue), animated: false) + } + } + var integerValue: Int { + get { + return Int(value) + } + set { + setValue(Float(newValue), animated: false) + } + } + var doubleValue: Double { + get { + return Double(value) + } + set { + setValue(Float(newValue), animated: false) + } + } + #else + var minimumValue: Float { + get { + return Float(minValue) + } + set { + minValue = Double(newValue) + } + } + var maximumValue: Float { + get { + return Float(maxValue) + } + set { + maxValue = Double(newValue) + } + } + #endif +} + +//MARK: - SegmentedControl +#if os(iOS) +typealias AGPopSheetButton = UIButton +#else +typealias AGPopSheetButton = NSPopUpButton +#endif + +//MARK: - SegmentedControl +#if os(iOS) +typealias AGSegmentedControl = UISegmentedControl +#else +typealias AGSegmentedControl = NSPopUpButton +#endif +#if os(macOS) +extension AGSegmentedControl { + var selectedSegmentIndex: Int { + get { + return indexOfSelectedItem + } + set { + selectItem(at: newValue) + } + } +} +#endif + +//MARK: - StoryboardSegue +#if os(iOS) +typealias AGStoryboardSegue = UIStoryboardSegue +#else +typealias AGStoryboardSegue = NSStoryboardSegue +#endif +extension AGStoryboardSegue { + var identifierString: String? { + get { + #if os(iOS) + return identifier + #else + return identifier + #endif + } + } + + #if os(iOS) + var destinationController: AGViewController? { + get { + return destination + } + } + #endif +} + +//MARK: - Storyboard +#if os(iOS) +typealias AGStoryboard = UIStoryboard +#else +typealias AGStoryboard = NSStoryboard +#endif + +//MARK: - ViewController +#if os(iOS) +typealias AGViewController = UIViewController +#else +typealias AGViewController = NSViewController +#endif +extension AGViewController { + #if os(OSX) + var title: String? { + get { + return self.view.window?.title + } + set { + guard let title = newValue else { + return + } + self.view.window?.title = title + } + } + #endif + + func performAGSegue(withIdentifier identifier: String, sender: Any?) { + #if os(iOS) + performSegue(withIdentifier: identifier, sender: sender) + #else + performSegue(withIdentifier: identifier, sender: sender) + #endif + } + + func dismissVC(_ vc: AGViewController, animated: Bool) { + #if os(iOS) + vc.dismiss(animated: animated, completion: nil) + #else + dismiss(nil) + #endif + } +} + +//MARK: - TableViewController +#if os(iOS) +typealias AGTableViewController = UITableViewController +#else +typealias AGTableViewController = NSViewController +#endif + + +#if os(iOS) +typealias AGBezierPath = UIBezierPath +#else +typealias AGBezierPath = NSBezierPath +#endif + +extension AGBezierPath { + #if os(OSX) + func addLine(to point: CGPoint) { + var points = [point] + self.appendPoints(&points, count: 1) + } + + func addArc(withCenter center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool) { + self.appendArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise) + } + #endif +} + +#if os(iOS) +typealias AGControl = UIControl +#else +typealias AGControl = NSControl +#endif + + +#if os(OSX) +extension String { + func buttonWhiteAttributedTitleString() -> NSAttributedString { + return buttonAttributedTitleStringWithColor(AGColor.white) + } + + func buttonBlueAttributedTitleString() -> NSAttributedString { + return buttonAttributedTitleStringWithColor(AGColor(hex: 0x00a0e9)) + } + + fileprivate func buttonAttributedTitleStringWithColor(_ color: AGColor) -> NSAttributedString { + let attributes = [NSAttributedString.Key.foregroundColor: color, NSAttributedString.Key.font: NSFont.systemFont(ofSize: 13)] + let attributedString = NSMutableAttributedString(string: self) + let range = NSMakeRange(0, attributedString.length) + attributedString.addAttributes(attributes, range: range) + attributedString.setAlignment(.center, range: range) + attributedString.fixAttributes(in: range) + + return attributedString + } +} +#endif + +#if os(iOS) +typealias AGApplication = UIApplication +#else +typealias AGApplication = NSApplication +#endif + diff --git a/iOS/APIExample/Common/VideoView.swift b/iOS/APIExample/Common/VideoView.swift index e46bc7e97..a24031381 100644 --- a/iOS/APIExample/Common/VideoView.swift +++ b/iOS/APIExample/Common/VideoView.swift @@ -1,92 +1,93 @@ // // VideoView.swift -// OpenVideoCall +// APIExample // -// Created by GongYuhua on 2/14/16. -// Copyright © 2016 Agora. All rights reserved. +// Created by 张乾泽 on 2020/9/16. +// Copyright © 2020 Agora Corp. All rights reserved. // import UIKit -class VideoView: UIView { - - fileprivate(set) var videoView: UIView! - - fileprivate var infoView: UIView! - fileprivate var infoLabel: UILabel! - - var isVideoMuted = false { - didSet { - videoView?.isHidden = isVideoMuted +extension Bundle { + + static func loadView(fromNib name: String, withType type: T.Type) -> T { + if let view = Bundle.main.loadNibNamed(name, owner: nil, options: nil)?.first as? T { + return view } + + fatalError("Could not load view with type " + String(describing: type)) } - override init(frame frameRect: CGRect) { - super.init(frame: frameRect) - translatesAutoresizingMaskIntoConstraints = false - backgroundColor = UIColor.white + static func loadVideoView(type:VideoView.StreamType, audioOnly:Bool) -> VideoView { + let view = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + view.audioOnly = audioOnly + view.type = type + if(type.isLocal()) { + view.statsInfo = StatisticsInfo(type: .local(StatisticsInfo.LocalInfo())) + } else { + view.statsInfo = StatisticsInfo(type: .remote(StatisticsInfo.RemoteInfo())) + } + return view + } +} + +class VideoView: UIView { + + @IBOutlet weak var videoView:UIView! + @IBOutlet weak var placeholderLabel:UILabel! + @IBOutlet weak var infoLabel:UILabel! + @IBOutlet weak var statsLabel:UILabel! + var audioOnly:Bool = false + var uid:UInt = 0 + enum StreamType { + case local + case remote - addVideoView() - addInfoView() + func isLocal() -> Bool{ + switch self { + case .local: return true + case .remote: return false + } + } + } + var statsInfo:StatisticsInfo? { + didSet{ + statsLabel.text = statsInfo?.description(audioOnly: audioOnly) + } + } + var type:StreamType? + + func setPlaceholder(text:String) { + placeholderLabel.text = text } - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + func setInfo(text:String) { + infoLabel.text = text + } + + override func awakeFromNib() { + super.awakeFromNib() + statsLabel.layer.shadowColor = UIColor.appColor(.textShadow)?.cgColor + statsLabel.layer.shadowOffset = CGSize(width: 1, height: 1) + statsLabel.layer.shadowRadius = 1.0 + statsLabel.layer.shadowOpacity = 0.7 } } -extension VideoView { - func update(with info: StatisticsInfo) { - infoLabel?.text = info.description() +class MetalVideoView: UIView { + @IBOutlet weak var placeholder: UILabel! + @IBOutlet weak var videoView: AgoraMetalRender! + @IBOutlet weak var infolabel: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() } -} -private extension VideoView { - func addVideoView() { - videoView = UIView() - videoView.translatesAutoresizingMaskIntoConstraints = false - videoView.backgroundColor = UIColor.clear - addSubview(videoView) - - let videoViewH = NSLayoutConstraint.constraints(withVisualFormat: "H:|[video]|", options: [], metrics: nil, views: ["video": videoView]) - let videoViewV = NSLayoutConstraint.constraints(withVisualFormat: "V:|[video]|", options: [], metrics: nil, views: ["video": videoView]) - NSLayoutConstraint.activate(videoViewH + videoViewV) + func setPlaceholder(text:String) { + placeholder.text = text } - func addInfoView() { - infoView = UIView() - infoView.translatesAutoresizingMaskIntoConstraints = false - infoView.backgroundColor = UIColor.clear - - addSubview(infoView) - let infoViewH = NSLayoutConstraint.constraints(withVisualFormat: "H:|[info]|", options: [], metrics: nil, views: ["info": infoView]) - let infoViewV = NSLayoutConstraint.constraints(withVisualFormat: "V:[info(==140)]|", options: [], metrics: nil, views: ["info": infoView]) - NSLayoutConstraint.activate(infoViewH + infoViewV) - - func createInfoLabel() -> UILabel { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - - label.text = " " - label.shadowOffset = CGSize(width: 0, height: 1) - label.shadowColor = UIColor.black - label.numberOfLines = 0 - - label.font = UIFont.systemFont(ofSize: 12) - label.textColor = UIColor.white - - return label - } - - infoLabel = createInfoLabel() - infoView.addSubview(infoLabel) - - let top: CGFloat = 20 - let left: CGFloat = 10 - - let labelV = NSLayoutConstraint.constraints(withVisualFormat: "V:|-(\(top))-[info]", options: [], metrics: nil, views: ["info": infoLabel]) - let labelH = NSLayoutConstraint.constraints(withVisualFormat: "H:|-(\(left))-[info]", options: [], metrics: nil, views: ["info": infoLabel]) - NSLayoutConstraint.activate(labelV) - NSLayoutConstraint.activate(labelH) + func setInfo(text:String) { + infolabel.text = text } } diff --git a/iOS/APIExample/Common/VideoView.xib b/iOS/APIExample/Common/VideoView.xib new file mode 100644 index 000000000..017e54772 --- /dev/null +++ b/iOS/APIExample/Common/VideoView.xib @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Common/VideoViewMetal.xib b/iOS/APIExample/Common/VideoViewMetal.xib new file mode 100644 index 000000000..f48e76890 --- /dev/null +++ b/iOS/APIExample/Common/VideoViewMetal.xib @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/ARKit/ARKit.swift b/iOS/APIExample/Examples/Advanced/ARKit/ARKit.swift new file mode 100644 index 000000000..ab4fe7694 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/ARKit/ARKit.swift @@ -0,0 +1,333 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import UIKit +import AgoraRtcKit +import ARKit + +class ARKitEntry : UIViewController +{ + @IBOutlet weak var joinButton: AGButton! + @IBOutlet weak var channelTextField: AGTextField! + let identifier = "ARKit" + + override func viewDidLoad() { + super.viewDidLoad() + } + + @IBAction func doJoinPressed(sender: AGButton) { + guard let channelName = channelTextField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName":channelName] + self.navigationController?.pushViewController(newViewController, animated: true) + } +} + +class ARKitMain: BaseViewController { + @IBOutlet weak var sceneView: ARSCNView! + @IBOutlet weak var infoLabel: UILabel! + var agoraKit: AgoraRtcEngineKit! + + fileprivate let videoSource = ARVideoSource() + fileprivate var unusedScreenNodes = [SCNNode]() + fileprivate var undisplayedUsers = [UInt]() + fileprivate var activeScreens = [UInt: SCNNode]() + + // indicate if current instance has joined channel + var isJoined: Bool = false + var planarDetected: Bool = false { + didSet { + if(planarDetected) { + infoLabel.text = "Tap to place remote video canvas".localized + } else { + infoLabel.text = "Move Camera to find a planar\n(Shown as Red Rectangle)".localized + } + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + //set AR Scene delegate + sceneView.delegate = self + sceneView.session.delegate = self + sceneView.showsStatistics = true + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + // get channel name from configs + guard let channelName = configs["channelName"] as? String else {return} + + // make myself a broadcaster + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) + + + // enable video module and set up video encoding configs + agoraKit.enableVideo() + agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: AgoraVideoDimension640x360, + frameRate: .fps60, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative)) + + + // set AR video source as custom video source + agoraKit.setVideoSource(videoSource) + // start AR Session + startARSession() + + + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channelName, info: nil, uid: 0, options: option) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + // start AR World tracking + func startARSession() { + guard ARWorldTrackingConfiguration.isSupported else { + showAlert(title: "ARKit is not available on this device.".localized, message: "This app requires world tracking, which is available only on iOS devices with the A9 processor or later.".localized) + return + } + + let configuration = ARWorldTrackingConfiguration() + configuration.planeDetection = .horizontal + // remember to set this to false, or ARKit may conflict with Agora SDK + configuration.providesAudioData = false + + // start session + sceneView.session.run(configuration) + } + + // stop AR Tracking + func stopARSession() { + sceneView.session.pause() + } + + @IBAction func doSceneViewTapped(_ recognizer: UITapGestureRecognizer) { + if(!planarDetected) { + LogUtils.log(message: "Planar not yet found", level: .warning) + return + } + + let location = recognizer.location(in: sceneView) + + if let node = sceneView.hitTest(location, options: nil).first?.node { + removeNode(node) + } else if let result = sceneView.hitTest(location, types: .existingPlane).first { + addNode(withTransform: result.worldTransform) + } + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + if isJoined { + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + stopARSession() + } + } +} + +private extension ARKitMain { + func renderRemoteUser(uid: UInt, toNode node: SCNNode) { + let renderer = ARVideoRenderer() + renderer.renderNode = node + activeScreens[uid] = node + + agoraKit.setRemoteVideoRenderer(renderer, forUserId: uid) + } + + func addNode(withTransform transform: matrix_float4x4) { + let scene = SCNScene(named: "AR.scnassets/displayer.scn")! + let rootNode = scene.rootNode + + rootNode.position = SCNVector3( + transform.columns.3.x, + transform.columns.3.y, + transform.columns.3.z + ) + rootNode.rotation = SCNVector4(0, 1, 0, sceneView.session.currentFrame!.camera.eulerAngles.y) + + sceneView.scene.rootNode.addChildNode(rootNode) + + let displayer = rootNode.childNode(withName: "displayer", recursively: false)! + let screen = displayer.childNode(withName: "screen", recursively: false)! + + if let undisplayedUid = undisplayedUsers.first { + undisplayedUsers.removeFirst() + renderRemoteUser(uid: undisplayedUid, toNode: screen) + } else { + unusedScreenNodes.append(screen) + } + } + + func removeNode(_ node: SCNNode) { + let rootNode: SCNNode + let screen: SCNNode + + if node.name == "screen", let parent = node.parent?.parent { + rootNode = parent + screen = node + } else if node.name == "displayer", let parent = node.parent { + rootNode = parent + screen = parent.childNode(withName: "screen", recursively: false)! + } else { + rootNode = node + screen = node + } + + rootNode.removeFromParentNode() + + if let index = unusedScreenNodes.firstIndex(where: {$0 == screen}) { + unusedScreenNodes.remove(at: index) + } + + if let (uid, _) = activeScreens.first(where: {$1 == screen}) { + activeScreens.removeValue(forKey: uid) + if let screenNode = unusedScreenNodes.first { + unusedScreenNodes.removeFirst() + renderRemoteUser(uid: uid, toNode: screenNode) + } else { + undisplayedUsers.insert(uid, at: 0) + } + } + } +} + +/// agora rtc engine delegate events +extension ARKitMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + if let screenNode = unusedScreenNodes.first { + unusedScreenNodes.removeFirst() + renderRemoteUser(uid: uid, toNode: screenNode) + } else { + undisplayedUsers.append(uid) + } + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + if let screenNode = activeScreens[uid] { + agoraKit.setRemoteVideoRenderer(nil, forUserId: uid) + unusedScreenNodes.insert(screenNode, at: 0) + activeScreens[uid] = nil + } else if let index = undisplayedUsers.firstIndex(of: uid) { + undisplayedUsers.remove(at: index) + } + } +} + + +extension ARKitMain: ARSCNViewDelegate { + func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { + guard let planeAnchor = anchor as? ARPlaneAnchor else { + return + } + + let plane = SCNBox(width: CGFloat(planeAnchor.extent.x), + height: CGFloat(planeAnchor.extent.y), + length: CGFloat(planeAnchor.extent.z), + chamferRadius: 0) + plane.firstMaterial?.diffuse.contents = UIColor.red + + let planeNode = SCNNode(geometry: plane) + node.addChildNode(planeNode) + planeNode.runAction(SCNAction.fadeOut(duration: 3)) + + //found planar + if(!planarDetected) { + DispatchQueue.main.async {[weak self] in + guard let weakSelf = self else { + return + } + weakSelf.planarDetected = true + } + } + } +} + +extension ARKitMain: ARSessionDelegate { + func session(_ session: ARSession, didUpdate frame: ARFrame) { + // send captured image to remote device + // note this video data DOES NOT contain AR info + videoSource.sendBuffer(frame.capturedImage, timestamp: frame.timestamp) + } +} diff --git a/iOS/APIExample/Examples/Advanced/ARKit/Base.lproj/ARKit.storyboard b/iOS/APIExample/Examples/Advanced/ARKit/Base.lproj/ARKit.storyboard new file mode 100644 index 000000000..d39e84c86 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/ARKit/Base.lproj/ARKit.storyboard @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/ARKit/zh-Hans.lproj/ARKit.strings b/iOS/APIExample/Examples/Advanced/ARKit/zh-Hans.lproj/ARKit.strings new file mode 100644 index 000000000..ae717e8b9 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/ARKit/zh-Hans.lproj/ARKit.strings @@ -0,0 +1,9 @@ + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UILabel"; text = "Move Camera to find a planar 
(Shown as Red Rectangle)"; ObjectID = "bEC-x6-7dT"; */ +"bEC-x6-7dT.text" = "移动相机以找到一个平面 
(以红色方块显示)"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; diff --git a/iOS/APIExample/Examples/Advanced/AudioMixing/AudioMixing.swift b/iOS/APIExample/Examples/Advanced/AudioMixing/AudioMixing.swift new file mode 100644 index 000000000..949a67b1c --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/AudioMixing/AudioMixing.swift @@ -0,0 +1,389 @@ +// +// AudioMixingMain.swift +// APIExample +// +// Created by ADMIN on 2020/5/18. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import UIKit +import AgoraRtcKit +import AGEVideoLayout + +let EFFECT_ID:Int32 = 1 + +class AudioMixingEntry : UIViewController +{ + @IBOutlet weak var joinButton: AGButton! + @IBOutlet weak var channelTextField: AGTextField! + @IBOutlet weak var scenarioBtn: UIButton! + @IBOutlet weak var profileBtn: UIButton! + var profile:AgoraAudioProfile = .default + var scenario:AgoraAudioScenario = .default + let identifier = "AudioMixing" + + override func viewDidLoad() { + super.viewDidLoad() + + profileBtn.setTitle("\(profile.description())", for: .normal) + scenarioBtn.setTitle("\(scenario.description())", for: .normal) + } + + @IBAction func doJoinPressed(sender: AGButton) { + guard let channelName = channelTextField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName":channelName, "audioProfile":profile, "audioScenario":scenario] + self.navigationController?.pushViewController(newViewController, animated: true) + } + + func getAudioProfileAction(_ profile:AgoraAudioProfile) -> UIAlertAction{ + return UIAlertAction(title: "\(profile.description())", style: .default, handler: {[unowned self] action in + self.profile = profile + self.profileBtn.setTitle("\(profile.description())", for: .normal) + }) + } + + func getAudioScenarioAction(_ scenario:AgoraAudioScenario) -> UIAlertAction{ + return UIAlertAction(title: "\(scenario.description())", style: .default, handler: {[unowned self] action in + self.scenario = scenario + self.scenarioBtn.setTitle("\(scenario.description())", for: .normal) + }) + } + + @IBAction func setAudioProfile(){ + let alert = UIAlertController(title: "Set Audio Profile".localized, message: nil, preferredStyle: .actionSheet) + for profile in AgoraAudioProfile.allValues(){ + alert.addAction(getAudioProfileAction(profile)) + } + alert.addCancelAction() + present(alert, animated: true, completion: nil) + } + + @IBAction func setAudioScenario(){ + let alert = UIAlertController(title: "Set Audio Scenario".localized, message: nil, preferredStyle: .actionSheet) + for scenario in AgoraAudioScenario.allValues(){ + alert.addAction(getAudioScenarioAction(scenario)) + } + alert.addCancelAction() + present(alert, animated: true, completion: nil) + } +} + +class AudioMixingMain: BaseViewController { + var agoraKit: AgoraRtcEngineKit! + @IBOutlet weak var container: AGEVideoContainer! + @IBOutlet weak var audioMixingVolumeSlider: UISlider! + @IBOutlet weak var audioMixingPlaybackVolumeSlider: UISlider! + @IBOutlet weak var audioMixingPublishVolumeSlider: UISlider! + @IBOutlet weak var audioMixingProgressView: UIProgressView! + @IBOutlet weak var audioMixingDuration: UILabel! + @IBOutlet weak var audioEffectVolumeSlider: UISlider! + var audioViews: [UInt:VideoView] = [:] + var timer:Timer? + + // indicate if current instance has joined channel + var isJoined: Bool = false + + override func viewDidLoad(){ + super.viewDidLoad() + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + guard let channelName = configs["channelName"] as? String, + let audioProfile = configs["audioProfile"] as? AgoraAudioProfile, + let audioScenario = configs["audioScenario"] as? AgoraAudioScenario + else {return} + + // make myself a broadcaster + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) + + // update slider values + audioMixingPlaybackVolumeSlider.setValue(Float(agoraKit.getAudioMixingPlayoutVolume()), animated: true) + audioMixingPublishVolumeSlider.setValue(Float(agoraKit.getAudioMixingPublishVolume()), animated: true) + audioEffectVolumeSlider.setValue(Float(agoraKit.getEffectsVolume()), animated: true) + + // disable video module + agoraKit.disableVideo() + + // set audio profile/audio scenario + agoraKit.setAudioProfile(audioProfile, scenario: audioScenario) + + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + + // enable volume indicator + agoraKit.enableAudioVolumeIndication(200, smooth: 3, report_vad: false) + + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channelName, info: nil, uid: 0, options: option) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + if isJoined { + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + } + } + + func sortedViews() -> [VideoView] { + return Array(audioViews.values).sorted(by: { $0.uid < $1.uid }) + } + + @IBAction func onChangeAudioMixingVolume(_ sender:UISlider){ + let value:Int = Int(sender.value) + print("adjustAudioMixingVolume \(value)") + agoraKit.adjustAudioMixingVolume(value) + } + + @IBAction func onChangeAudioMixingPlaybackVolume(_ sender:UISlider){ + let value:Int = Int(sender.value) + print("adjustAudioMixingPlayoutVolume \(value)") + agoraKit.adjustAudioMixingPlayoutVolume(value) + } + + @IBAction func onChangeAudioMixingPublishVolume(_ sender:UISlider){ + let value:Int = Int(sender.value) + print("adjustAudioMixingPublishVolume \(value)") + agoraKit.adjustAudioMixingPublishVolume(value) + } + + @IBAction func onChangeAudioEffectVolume(_ sender:UISlider){ + let value:Int = Int(sender.value) + print("setEffectsVolume \(value)") + agoraKit.setEffectsVolume(Double(value)) + } + + @IBAction func onStartAudioMixing(_ sender:UIButton){ + if let filepath = Bundle.main.path(forResource: "audiomixing", ofType: "mp3") { + let result = agoraKit.startAudioMixing(filepath, loopback: false, replace: false, cycle: -1) + if result != 0 { + self.showAlert(title: "Error", message: "startAudioMixing call failed: \(result), please check your params") + } else { + startProgressTimer() + updateTotalDuration(reset: false) + } + } + } + + @IBAction func onStopAudioMixing(_ sender:UIButton){ + let result = agoraKit.stopAudioMixing() + if result != 0 { + self.showAlert(title: "Error", message: "stopAudioMixing call failed: \(result), please check your params") + } else { + stopProgressTimer() + updateTotalDuration(reset: true) + } + } + + @IBAction func onPauseAudioMixing(_ sender:UIButton){ + let result = agoraKit.pauseAudioMixing() + if result != 0 { + self.showAlert(title: "Error", message: "pauseAudioMixing call failed: \(result), please check your params") + } else { + stopProgressTimer() + } + } + + @IBAction func onResumeAudioMixing(_ sender:UIButton){ + let result = agoraKit.resumeAudioMixing() + if result != 0 { + self.showAlert(title: "Error", message: "resumeAudioMixing call failed: \(result), please check your params") + } else { + startProgressTimer() + } + } + + func startProgressTimer() { + // begin timer to update progress + if(timer == nil) { + timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self](timer:Timer) in + guard let weakself = self else {return} + let progress = Float(weakself.agoraKit.getAudioMixingCurrentPosition()) / Float(weakself.agoraKit.getAudioMixingDuration()) + weakself.audioMixingProgressView.setProgress(progress, animated: true) + }) + } + } + + func stopProgressTimer() { + // stop timer + if(timer != nil) { + timer?.invalidate() + timer = nil + } + } + + func updateTotalDuration(reset:Bool) { + if(reset) { + audioMixingDuration.text = "00 : 00" + } else { + let duration = agoraKit.getAudioMixingDuration() + let seconds = duration / 1000 + audioMixingDuration.text = "\(String(format: "%02d", seconds / 60)) : \(String(format: "%02d", seconds % 60))" + } + } + + @IBAction func onPlayEffect(_ sender:UIButton){ + if let filepath = Bundle.main.path(forResource: "audioeffect", ofType: "mp3") { + let result = agoraKit.playEffect(EFFECT_ID, filePath: filepath, loopCount: -1, pitch: 1, pan: 0, gain: 100, publish: true) + if result != 0 { + self.showAlert(title: "Error", message: "playEffect call failed: \(result), please check your params") + } + } + } + + @IBAction func onStopEffect(_ sender:UIButton){ + let result = agoraKit.stopEffect(EFFECT_ID) + if result != 0 { + self.showAlert(title: "Error", message: "stopEffect call failed: \(result), please check your params") + } + } + + @IBAction func onPauseEffect(_ sender:UIButton){ + let result = agoraKit.pauseEffect(EFFECT_ID) + if result != 0 { + self.showAlert(title: "Error", message: "pauseEffect call failed: \(result), please check your params") + } + } + + @IBAction func onResumeEffect(_ sender:UIButton){ + let result = agoraKit.resumeEffect(EFFECT_ID) + if result != 0 { + self.showAlert(title: "Error", message: "resumeEffect call failed: \(result), please check your params") + } + } +} + +/// agora rtc engine delegate events +extension AudioMixingMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + + //set up local audio view, this view will not show video but just a placeholder + let view = Bundle.loadVideoView(type: .local, audioOnly: true) + audioViews[0] = view + view.setPlaceholder(text: self.getAudioLabel(uid: uid, isLocal: true)) + container.layoutStream2x1(views: self.sortedViews()) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + //set up remote audio view, this view will not show video but just a placeholder + let view = Bundle.loadVideoView(type: .remote, audioOnly: true) + view.uid = uid + self.audioViews[uid] = view + view.setPlaceholder(text: self.getAudioLabel(uid: uid, isLocal: false)) + self.container.layoutStream2x1(views: sortedViews()) + self.container.reload(level: 0, animated: true) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + //remove remote audio view + self.audioViews.removeValue(forKey: uid) + self.container.layoutStream2x1(views: sortedViews()) + self.container.reload(level: 0, animated: true) + } + + /// Reports which users are speaking, the speakers' volumes, and whether the local user is speaking. + /// @params speakers volume info for all speakers + /// @params totalVolume Total volume after audio mixing. The value range is [0,255]. + func rtcEngine(_ engine: AgoraRtcEngineKit, reportAudioVolumeIndicationOfSpeakers speakers: [AgoraRtcAudioVolumeInfo], totalVolume: Int) { + for volumeInfo in speakers { + if let audioView = audioViews[volumeInfo.uid] { + audioView.setInfo(text: "Volume:\(volumeInfo.volume)") + } + } + } + + /// Reports the statistics of the current call. The SDK triggers this callback once every two seconds after the user joins the channel. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, reportRtcStats stats: AgoraChannelStats) { + audioViews[0]?.statsInfo?.updateChannelStats(stats) + } + + /// Reports the statistics of the uploading local audio streams once every two seconds. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, localAudioStats stats: AgoraRtcLocalAudioStats) { + audioViews[0]?.statsInfo?.updateLocalAudioStats(stats) + } + + /// Reports the statistics of the audio stream from each remote user/host. + /// @param stats stats struct for current call statistics + func rtcEngine(_ engine: AgoraRtcEngineKit, remoteAudioStats stats: AgoraRtcRemoteAudioStats) { + audioViews[stats.uid]?.statsInfo?.updateAudioStats(stats) + } + + func rtcEngine(_ engine: AgoraRtcEngineKit, localAudioMixingStateDidChanged state: AgoraAudioMixingStateCode, errorCode: AgoraAudioMixingErrorCode) { + LogUtils.log(message: " --- \(state.rawValue) \(errorCode.rawValue)", level: .info) + } +} diff --git a/iOS/APIExample/Examples/Advanced/AudioMixing/Base.lproj/AudioMixing.storyboard b/iOS/APIExample/Examples/Advanced/AudioMixing/Base.lproj/AudioMixing.storyboard new file mode 100644 index 000000000..07a582b92 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/AudioMixing/Base.lproj/AudioMixing.storyboard @@ -0,0 +1,395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings b/iOS/APIExample/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings new file mode 100644 index 000000000..acb49bbe7 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings @@ -0,0 +1,66 @@ + +/* Class = "UILabel"; text = "MixingPlaybackVolume"; ObjectID = "07c-He-s8j"; */ +"07c-He-s8j.text" = "混音播放音量"; + +/* Class = "UIButton"; normalTitle = "Pause"; ObjectID = "1zo-J9-vQy"; */ +"1zo-J9-vQy.normalTitle" = "暂停"; + +/* Class = "UILabel"; text = "Audio Mixing Controls"; ObjectID = "4Y1-AZ-KwW"; */ +"4Y1-AZ-KwW.text" = "混音控制"; + +/* Class = "UIButton"; normalTitle = "Stop"; ObjectID = "54l-lw-iap"; */ +"54l-lw-iap.normalTitle" = "停止"; + +/* Class = "UILabel"; text = "Audio Effect Controls"; ObjectID = "5o8-Cv-WLg"; */ +"5o8-Cv-WLg.text" = "音效控制"; + +/* Class = "UIButton"; normalTitle = "Resume"; ObjectID = "CRH-0X-9T4"; */ +"CRH-0X-9T4.normalTitle" = "恢复播放"; + +/* Class = "UILabel"; text = "MixingVolume"; ObjectID = "DJt-Y7-fkM"; */ +"DJt-Y7-fkM.text" = "混音音量"; + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UIButton"; normalTitle = "Start"; ObjectID = "J8R-TU-x8W"; */ +"J8R-TU-x8W.normalTitle" = "开始"; + +/* Class = "UILabel"; text = "Audio Scenario"; ObjectID = "Q0E-5B-IED"; */ +"Q0E-5B-IED.text" = "音频使用场景"; + +/* Class = "UILabel"; text = "MixingPublishVolume"; ObjectID = "VMe-lv-SUb"; */ +"VMe-lv-SUb.text" = "混音发布音量"; + +/* Class = "UILabel"; text = "00 : 00"; ObjectID = "cJ6-0Q-fAp"; */ +"cJ6-0Q-fAp.text" = "00 : 00"; + +/* Class = "UILabel"; text = "EffectVolume"; ObjectID = "e6E-so-zA5"; */ +"e6E-so-zA5.text" = "音效音量"; + +/* Class = "UILabel"; text = "Audio Profile"; ObjectID = "iUn-XK-AS2"; */ +"iUn-XK-AS2.text" = "音频属性配置"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "iZP-Ce-Oxt"; */ +"iZP-Ce-Oxt.normalTitle" = "Button"; + +/* Class = "UIButton"; normalTitle = "Resume"; ObjectID = "jRA-VE-1PM"; */ +"jRA-VE-1PM.normalTitle" = "恢复播放"; + +/* Class = "UIViewController"; title = "Join Channel Audio"; ObjectID = "jxp-ZN-2yG"; */ +"jxp-ZN-2yG.title" = "Join Channel Audio"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; + +/* Class = "UIButton"; normalTitle = "Play"; ObjectID = "m2n-wi-5Xx"; */ +"m2n-wi-5Xx.normalTitle" = "播放"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "myR-6e-1zj"; */ +"myR-6e-1zj.normalTitle" = "Button"; + +/* Class = "UIButton"; normalTitle = "Stop"; ObjectID = "nzY-OP-Heo"; */ +"nzY-OP-Heo.normalTitle" = "停止"; + +/* Class = "UIButton"; normalTitle = "Pause"; ObjectID = "u26-Qh-itu"; */ +"u26-Qh-itu.normalTitle" = "暂停"; diff --git a/iOS/APIExample/Examples/Advanced/CreateDataStream/Base.lproj/CreateDataStream.storyboard b/iOS/APIExample/Examples/Advanced/CreateDataStream/Base.lproj/CreateDataStream.storyboard new file mode 100644 index 000000000..a3a710c5c --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/CreateDataStream/Base.lproj/CreateDataStream.storyboard @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/CreateDataStream/CreateDataStream.swift b/iOS/APIExample/Examples/Advanced/CreateDataStream/CreateDataStream.swift new file mode 100644 index 000000000..7de25caf1 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/CreateDataStream/CreateDataStream.swift @@ -0,0 +1,244 @@ +// +// CreateDataStream.swift +// APIExample +// +// Created by XC on 2020/12/28. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import UIKit +import AGEVideoLayout +import AgoraRtcKit + +class CreateDataStreamEntry: UIViewController { + @IBOutlet weak var joinButton: UIButton! + @IBOutlet weak var channelTextField: UITextField! + let identifier = "CreateDataStream" + + override func viewDidLoad() { + super.viewDidLoad() + } + + @IBAction func doJoinPressed(sender: UIButton) { + guard let channelName = channelTextField.text else { return } + //resign channel text field + channelTextField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName": channelName] + self.navigationController?.pushViewController(newViewController, animated: true) + } +} + +class CreateDataStreamMain: BaseViewController { + var localVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + var remoteVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + + @IBOutlet weak var container: AGEVideoContainer! + @IBOutlet weak var sendButton: UIButton! + @IBOutlet weak var messageField: UITextField! + var agoraKit: AgoraRtcEngineKit! + + // indicate if current instance has joined channel + var isJoined: Bool = false + var isSending: Bool = false { + didSet { + sendButton.isEnabled = isJoined && !isSending + messageField.isEnabled = !isSending + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + // layout render view + localVideo.setPlaceholder(text: "Local Host".localized) + remoteVideo.setPlaceholder(text: "Remote Host".localized) + container.layoutStream(views: [localVideo, remoteVideo]) + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + // get channel name from configs + guard let channelName = configs["channelName"] as? String, + let resolution = GlobalSettings.shared.getSetting(key: "resolution")?.selectedOption().value as? CGSize, + let fps = GlobalSettings.shared.getSetting(key: "fps")?.selectedOption().value as? AgoraVideoFrameRate, + let orientation = GlobalSettings.shared.getSetting(key: "orientation")?.selectedOption().value as? AgoraVideoOutputOrientationMode else {return} + + + // make myself a broadcaster + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) + + // enable video module and set up video encoding configs + agoraKit.enableVideo() + agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: resolution, + frameRate: fps, + bitrate: AgoraVideoBitrateStandard, + orientationMode: orientation)) + + // set up local video to render your local camera preview + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + // the view to be binded + videoCanvas.view = localVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channelName, info: nil, uid: SCREEN_SHARE_BROADCASTER_UID, options: option) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + // indicate if stream has created + var streamCreated = false + var streamId: Int = 0 + + /// send message + @IBAction func onSendPress(_ sender: UIButton) { + if !isSending { + let message = messageField.text + if message == nil || message!.isEmpty { + return + } + isSending = true + if !streamCreated { + // create the data stream + // Each user can create up to five data streams during the lifecycle of the agoraKit + let config = AgoraDataStreamConfig() + let result = agoraKit.createDataStream(&streamId, config: config) + if result != 0 { + isSending = false + showAlert(title: "Error", message: "createDataStream call failed: \(result), please check your params") + } else { + streamCreated = true + } + } + + let result = agoraKit.sendStreamMessage(streamId, data: Data(message!.utf8)) + if result != 0 { + showAlert(title: "Error", message: "sendStreamMessage call failed: \(result), please check your params") + } else { + messageField.text = nil + } + isSending = false + } + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + if isJoined { + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + } + } +} + +/// agora rtc engine delegate events +extension CreateDataStreamMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // Only one remote video view is available for this + // tutorial. Here we check if there exists a surface + // view tagged as this uid. + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + } + + func rtcEngine(_ engine: AgoraRtcEngineKit, receiveStreamMessageFromUid uid: UInt, streamId: Int, data: Data) { + let message = String.init(data: data, encoding: .utf8) ?? "" + LogUtils.log(message: "receiveStreamMessageFromUid: \(uid) \(message)", level: .info) + showAlert(message: "from: \(uid) message: \(message)") + } + + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurStreamMessageErrorFromUid uid: UInt, streamId: Int, error: Int, missed: Int, cached: Int) { + LogUtils.log(message: "didOccurStreamMessageErrorFromUid: \(uid), error \(error), missed \(missed), cached \(cached)", level: .info) + showAlert(message: "didOccurStreamMessageErrorFromUid: \(uid)") + } +} diff --git a/iOS/APIExample/Examples/Advanced/CreateDataStream/zh-Hans.lproj/CreateDataStream.strings b/iOS/APIExample/Examples/Advanced/CreateDataStream/zh-Hans.lproj/CreateDataStream.strings new file mode 100644 index 000000000..3aa32e876 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/CreateDataStream/zh-Hans.lproj/CreateDataStream.strings @@ -0,0 +1,15 @@ + +/* Class = "UITextField"; placeholder = "Input Message"; ObjectID = "5E0-OO-sA5"; */ +"5E0-OO-sA5.placeholder" = "输入消息"; + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "HnX-Xj-hjt"; */ +"HnX-Xj-hjt.placeholder" = "输入频道名"; + +/* Class = "UIButton"; normalTitle = "Send"; ObjectID = "T9i-H1-PtG"; */ +"T9i-H1-PtG.normalTitle" = "发送"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "UF2-SD-j5U"; */ +"UF2-SD-j5U.normalTitle" = "加入频道"; + +/* Class = "UILabel"; text = "Send Message"; ObjectID = "ey2-dt-kXq"; */ +"ey2-dt-kXq.text" = "发送消息"; diff --git a/iOS/APIExample/Examples/Advanced/CustomAudioRender/Base.lproj/CustomAudioRender.storyboard b/iOS/APIExample/Examples/Advanced/CustomAudioRender/Base.lproj/CustomAudioRender.storyboard new file mode 100644 index 000000000..a52846133 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/CustomAudioRender/Base.lproj/CustomAudioRender.storyboard @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/CustomAudioRender/CustomAudioRender.swift b/iOS/APIExample/Examples/Advanced/CustomAudioRender/CustomAudioRender.swift new file mode 100644 index 000000000..0e38b5376 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/CustomAudioRender/CustomAudioRender.swift @@ -0,0 +1,181 @@ +// +// CustomAudioSource.swift +// APIExample +// +// Created by 张乾泽 on 2020/7/28. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import Foundation +import AgoraRtcKit +import AGEVideoLayout + +class CustomAudioRenderEntry : UIViewController +{ + @IBOutlet weak var joinButton: AGButton! + @IBOutlet weak var channelTextField: AGTextField! + let identifier = "CustomAudioRender" + + override func viewDidLoad() { + super.viewDidLoad() + } + + @IBAction func doJoinPressed(sender: AGButton) { + guard let channelName = channelTextField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName":channelName] + self.navigationController?.pushViewController(newViewController, animated: true) + } +} + +class CustomAudioRenderMain: BaseViewController { + var agoraKit: AgoraRtcEngineKit! + var exAudio: ExternalAudio = ExternalAudio.shared() + @IBOutlet weak var container: AGEVideoContainer! + var audioViews: [UInt:VideoView] = [:] + + // indicate if current instance has joined channel + var isJoined: Bool = false + + override func viewDidLoad(){ + super.viewDidLoad() + + let sampleRate:UInt = 44100, channel:UInt = 1 + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + guard let channelName = configs["channelName"] as? String else {return} + + // make myself a broadcaster + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) + + // disable video module + agoraKit.disableVideo() + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) + + // setup external audio source + exAudio.setupExternalAudio(withAgoraKit: agoraKit, sampleRate: UInt32(sampleRate), channels: UInt32(channel), audioCRMode: .sdkCaptureExterRender, ioType: .remoteIO) + // important!! this example is using onPlaybackAudioFrame to do custom rendering + // by default the audio output will still be processed by SDK hence below api call is mandatory to disable that behavior + agoraKit.setParameters("{\"che.audio.external_render\": true}") + agoraKit.setParameters("{\"che.audio.keep.audiosession\": true}") + + + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channelName, info: nil, uid: 0, options: option) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + if isJoined { + exAudio.stopWork() + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + } + } +} + +/// agora rtc engine delegate events +extension CustomAudioRenderMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + + exAudio.startWork() + + //set up local audio view, this view will not show video but just a placeholder + let view = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + audioViews[uid] = view + view.setPlaceholder(text: self.getAudioLabel(uid: uid, isLocal: true)) + container.layoutStream3x3(views: Array(self.audioViews.values)) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + //set up remote audio view, this view will not show video but just a placeholder + let view = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + self.audioViews[uid] = view + view.setPlaceholder(text: self.getAudioLabel(uid: uid, isLocal: false)) + self.container.layoutStream3x3(views: Array(self.audioViews.values)) + self.container.reload(level: 0, animated: true) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + //remove remote audio view + self.audioViews.removeValue(forKey: uid) + self.container.layoutStream3x3(views: Array(self.audioViews.values)) + self.container.reload(level: 0, animated: true) + } +} diff --git a/iOS/APIExample/Examples/Advanced/CustomAudioRender/zh-Hans.lproj/CustomAudioRender.strings b/iOS/APIExample/Examples/Advanced/CustomAudioRender/zh-Hans.lproj/CustomAudioRender.strings new file mode 100644 index 000000000..28b31d39e --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/CustomAudioRender/zh-Hans.lproj/CustomAudioRender.strings @@ -0,0 +1,9 @@ + +/* Class = "UIViewController"; title = "Join Channel Audio"; ObjectID = "EbX-sK-6UJ"; */ +"EbX-sK-6UJ.title" = "音频自渲染"; + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; diff --git a/iOS/APIExample/Examples/Advanced/CustomAudioSource/Base.lproj/CustomAudioSource.storyboard b/iOS/APIExample/Examples/Advanced/CustomAudioSource/Base.lproj/CustomAudioSource.storyboard new file mode 100644 index 000000000..03832d590 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/CustomAudioSource/Base.lproj/CustomAudioSource.storyboard @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/CustomAudioSource/CustomAudioSource.swift b/iOS/APIExample/Examples/Advanced/CustomAudioSource/CustomAudioSource.swift new file mode 100644 index 000000000..566756d60 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/CustomAudioSource/CustomAudioSource.swift @@ -0,0 +1,178 @@ +// +// CustomAudioSourceMain.swift +// APIExample +// +// Created by 张乾泽 on 2020/7/28. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import Foundation +import AgoraRtcKit +import AGEVideoLayout + +class CustomAudioSourceEntry : UIViewController +{ + @IBOutlet weak var joinButton: AGButton! + @IBOutlet weak var channelTextField: AGTextField! + let identifier = "CustomAudioSource" + + override func viewDidLoad() { + super.viewDidLoad() + } + + @IBAction func doJoinPressed(sender: AGButton) { + guard let channelName = channelTextField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName":channelName] + self.navigationController?.pushViewController(newViewController, animated: true) + } +} + +class CustomAudioSourceMain: BaseViewController { + var agoraKit: AgoraRtcEngineKit! + var exAudio: ExternalAudio = ExternalAudio.shared() + @IBOutlet weak var container: AGEVideoContainer! + var audioViews: [UInt:VideoView] = [:] + + // indicate if current instance has joined channel + var isJoined: Bool = false + + override func viewDidLoad(){ + super.viewDidLoad() + + let sampleRate:UInt = 44100, channel:UInt = 1 + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + guard let channelName = configs["channelName"] as? String else {return} + + // make myself a broadcaster + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) + + // disable video module + agoraKit.disableVideo() + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + + // setup external audio source + exAudio.setupExternalAudio(withAgoraKit: agoraKit, sampleRate: UInt32(sampleRate), channels: UInt32(channel), audioCRMode: .exterCaptureSDKRender, ioType: .remoteIO) + agoraKit.enableExternalAudioSource(withSampleRate: sampleRate, channelsPerFrame: channel) + + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channelName, info: nil, uid: 0, options: option) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + if isJoined { + exAudio.stopWork() + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + } + } +} + +/// agora rtc engine delegate events +extension CustomAudioSourceMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + + exAudio.startWork() + let sampleRate: Double = 44100 + try? AVAudioSession.sharedInstance().setPreferredSampleRate(sampleRate) + + //set up local audio view, this view will not show video but just a placeholder + let view = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + audioViews[uid] = view + view.setPlaceholder(text: self.getAudioLabel(uid: uid, isLocal: true)) + container.layoutStream3x3(views: Array(self.audioViews.values)) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + //set up remote audio view, this view will not show video but just a placeholder + let view = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + self.audioViews[uid] = view + view.setPlaceholder(text: self.getAudioLabel(uid: uid, isLocal: false)) + self.container.layoutStream3x3(views: Array(self.audioViews.values)) + self.container.reload(level: 0, animated: true) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + //remove remote audio view + self.audioViews.removeValue(forKey: uid) + self.container.layoutStream3x3(views: Array(self.audioViews.values)) + self.container.reload(level: 0, animated: true) + } +} diff --git a/iOS/APIExample/Examples/Advanced/CustomAudioSource/zh-Hans.lproj/CustomAudioSource.strings b/iOS/APIExample/Examples/Advanced/CustomAudioSource/zh-Hans.lproj/CustomAudioSource.strings new file mode 100644 index 000000000..c8107f814 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/CustomAudioSource/zh-Hans.lproj/CustomAudioSource.strings @@ -0,0 +1,9 @@ + +/* Class = "UIViewController"; title = "Join Channel Audio"; ObjectID = "FCW-Np-auB"; */ +"FCW-Np-auB.title" = "音频自采集"; + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; diff --git a/iOS/APIExample/Examples/Advanced/CustomVideoRender/Base.lproj/CustomVideoRender.storyboard b/iOS/APIExample/Examples/Advanced/CustomVideoRender/Base.lproj/CustomVideoRender.storyboard new file mode 100644 index 000000000..86eaea918 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/CustomVideoRender/Base.lproj/CustomVideoRender.storyboard @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/CustomVideoRender/CustomVideoRender.swift b/iOS/APIExample/Examples/Advanced/CustomVideoRender/CustomVideoRender.swift new file mode 100644 index 000000000..c05819f4a --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/CustomVideoRender/CustomVideoRender.swift @@ -0,0 +1,179 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import UIKit +import AGEVideoLayout +import AgoraRtcKit + +class CustomVideoRenderEntry : UIViewController +{ + @IBOutlet weak var joinButton: AGButton! + @IBOutlet weak var channelTextField: AGTextField! + let identifier = "CustomVideoRender" + + override func viewDidLoad() { + super.viewDidLoad() + } + + @IBAction func doJoinPressed(sender: AGButton) { + guard let channelName = channelTextField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName":channelName] + self.navigationController?.pushViewController(newViewController, animated: true) + } +} + +class CustomVideoRenderMain: BaseViewController { + var localVideo = Bundle.loadView(fromNib: "VideoViewMetal", withType: MetalVideoView.self) + var remoteVideo = Bundle.loadView(fromNib: "VideoViewMetal", withType: MetalVideoView.self) + + @IBOutlet weak var container: AGEVideoContainer! + var agoraKit: AgoraRtcEngineKit! + + // indicate if current instance has joined channel + var isJoined: Bool = false + + override func viewDidLoad() { + super.viewDidLoad() + // layout render view + localVideo.setPlaceholder(text: "Local Host".localized) + remoteVideo.setPlaceholder(text: "Remote Host".localized) + container.layoutStream(views: [localVideo, remoteVideo]) + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + // get channel name from configs + guard let channelName = configs["channelName"] as? String, + let resolution = GlobalSettings.shared.getSetting(key: "resolution")?.selectedOption().value as? CGSize, + let fps = GlobalSettings.shared.getSetting(key: "fps")?.selectedOption().value as? AgoraVideoFrameRate, + let orientation = GlobalSettings.shared.getSetting(key: "orientation")?.selectedOption().value as? AgoraVideoOutputOrientationMode else {return} + + // make myself a broadcaster + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) + + + // enable video module and set up video encoding configs + agoraKit.enableVideo() + agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: resolution, + frameRate: fps, + bitrate: AgoraVideoBitrateStandard, + orientationMode: orientation)) + + + // set up your own render + if let customRender = localVideo.videoView { + agoraKit.setLocalVideoRenderer(customRender) + } + + + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channelName, info: nil, uid: 0, options: option) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + if isJoined { + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + } + } +} + +/// agora rtc engine delegate events +extension CustomVideoRenderMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // Only one remote video view is available for this + // tutorial. Here we check if there exists a surface + // view tagged as this uid. + // set up your own render + if let customRender = remoteVideo.videoView { + agoraKit.setRemoteVideoRenderer(customRender, forUserId: uid) + } + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + agoraKit.setRemoteVideoRenderer(nil, forUserId: uid) + } +} diff --git a/iOS/APIExample/Examples/Advanced/CustomVideoRender/zh-Hans.lproj/CustomVideoRender.strings b/iOS/APIExample/Examples/Advanced/CustomVideoRender/zh-Hans.lproj/CustomVideoRender.strings new file mode 100644 index 000000000..f50003d46 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/CustomVideoRender/zh-Hans.lproj/CustomVideoRender.strings @@ -0,0 +1,12 @@ + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UINavigationItem"; title = "Join Channel"; ObjectID = "ZgN-iF-qYr"; */ +"ZgN-iF-qYr.title" = "Join Channel"; + +/* Class = "UIViewController"; title = "Join Channel Video"; ObjectID = "aGp-ad-ObV"; */ +"aGp-ad-ObV.title" = "视频自渲染(Metal)"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; diff --git a/iOS/APIExample/Examples/Advanced/CustomVideoSourceMediaIO/Base.lproj/CustomVideoSourceMediaIO.storyboard b/iOS/APIExample/Examples/Advanced/CustomVideoSourceMediaIO/Base.lproj/CustomVideoSourceMediaIO.storyboard new file mode 100644 index 000000000..0e2aa5480 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/CustomVideoSourceMediaIO/Base.lproj/CustomVideoSourceMediaIO.storyboard @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Basic-Video-Audio/JoinChannel.swift b/iOS/APIExample/Examples/Advanced/CustomVideoSourceMediaIO/CustomVideoSourceMediaIO.swift similarity index 51% rename from iOS/APIExample/Examples/Basic-Video-Audio/JoinChannel.swift rename to iOS/APIExample/Examples/Advanced/CustomVideoSourceMediaIO/CustomVideoSourceMediaIO.swift index 72d51a1fb..1c8bd400e 100644 --- a/iOS/APIExample/Examples/Basic-Video-Audio/JoinChannel.swift +++ b/iOS/APIExample/Examples/Advanced/CustomVideoSourceMediaIO/CustomVideoSourceMediaIO.swift @@ -5,76 +5,82 @@ // Created by 张乾泽 on 2020/4/17. // Copyright © 2020 Agora Corp. All rights reserved. // - -import Foundation import UIKit +import AGEVideoLayout import AgoraRtcKit - -class JoinChannelMain: BaseViewController { - @IBOutlet weak var joinButton: UIButton! - @IBOutlet weak var channelTextField: UITextField! - - var localVideo = VideoView(frame: CGRect.zero) - var remoteVideo = VideoView(frame: CGRect.zero) - - var agoraKit: AgoraRtcEngineKit! +class CustomVideoSourceMediaIOEntry : UIViewController +{ + @IBOutlet weak var joinButton: AGButton! + @IBOutlet weak var channelTextField: AGTextField! + let identifier = "CustomVideoSourceMediaIO" - // indicate if current instance has joined channel - var isJoined: Bool = false { - didSet { - channelTextField.isEnabled = !isJoined - joinButton.isHidden = isJoined - } - } - - override func viewDidLoad(){ + override func viewDidLoad() { super.viewDidLoad() - // set up agora instance when view loaded - agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: KeyCenter.AppId, delegate: self) } - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - // leave channel when exiting the view - if(isJoined) { - agoraKit.leaveChannel { (stats) -> Void in - LogUtils.log(msg: "left channel, duration: \(stats.duration)", level: .info) - } - } - } - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - guard let identifier = segue.identifier else { - return - } + @IBAction func doJoinPressed(sender: AGButton) { + guard let channelName = channelTextField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() - switch identifier { - case "RenderViewController": - let vc = segue.destination as! RenderViewController - vc.layoutStream(views: [localVideo, remoteVideo]) - default: - break - } + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName":channelName] + self.navigationController?.pushViewController(newViewController, animated: true) } +} + +class CustomVideoSourceMediaIOMain: BaseViewController { + var localVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + var remoteVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + fileprivate let customCamera = AgoraCameraSourceMediaIO() - override func touchesBegan(_ touches: Set, with event: UIEvent?) { - view.endEditing(true) - } + @IBOutlet weak var container: AGEVideoContainer! + var agoraKit: AgoraRtcEngineKit! - /// callback when join button hit - @IBAction func onJoin(){ - guard let channelName = channelTextField.text else {return} + // indicate if current instance has joined channel + var isJoined: Bool = false + + override func viewDidLoad() { + super.viewDidLoad() + // layout render view + localVideo.setPlaceholder(text: "Local Host".localized) + remoteVideo.setPlaceholder(text: "Remote Host".localized) + container.layoutStream(views: [localVideo, remoteVideo]) - //hide keyboard - channelTextField.resignFirstResponder() + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + // get channel name from configs + guard let channelName = configs["channelName"] as? String, + let resolution = GlobalSettings.shared.getSetting(key: "resolution")?.selectedOption().value as? CGSize, + let fps = GlobalSettings.shared.getSetting(key: "fps")?.selectedOption().value as? AgoraVideoFrameRate, + let orientation = GlobalSettings.shared.getSetting(key: "orientation")?.selectedOption().value as? AgoraVideoOutputOrientationMode else {return} + + // make myself a broadcaster + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) // enable video module and set up video encoding configs agoraKit.enableVideo() - agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: AgoraVideoDimension640x360, - frameRate: .fps15, + + // setup my own camera as custom video source + agoraKit.setVideoSource(customCamera) + agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: resolution, + frameRate: fps, bitrate: AgoraVideoBitrateStandard, - orientationMode: .adaptative)) + orientationMode: orientation)) // set up local video to render your local camera preview let videoCanvas = AgoraRtcVideoCanvas() @@ -93,22 +99,31 @@ class JoinChannelMain: BaseViewController { // 2. If app certificate is turned on at dashboard, token is needed // when joining channel. The channel name and uid used to calculate // the token has to match the ones used for channel join - let result = agoraKit.joinChannel(byToken: nil, channelId: channelName, info: nil, uid: 0) {[unowned self] (channel, uid, elapsed) -> Void in - self.isJoined = true - LogUtils.log(msg: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) - } - if(result != 0) { + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channelName, info: nil, uid: 0, options: option) + if result != 0 { // Usually happens with invalid parameters // Error code description can be found at: // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html - self.showAlert(title: "Error", msg: "joinChannel call failed: \(result), please check your params") + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + if isJoined { + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } } } } /// agora rtc engine delegate events -extension JoinChannelMain: AgoraRtcEngineDelegate { +extension CustomVideoSourceMediaIOMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: @@ -116,7 +131,7 @@ extension JoinChannelMain: AgoraRtcEngineDelegate { /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { - LogUtils.log(msg: "warning: \(warningCode.description)", level: .warning) + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) } /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand @@ -126,15 +141,25 @@ extension JoinChannelMain: AgoraRtcEngineDelegate { /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { - LogUtils.log(msg: "error: \(errorCode)", level: .error) - self.showAlert(title: "Error", msg: "Error \(errorCode.description) occur") + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) } /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event /// @param uid uid of remote joined user /// @param elapsed time elapse since current sdk instance join the channel in ms func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { - LogUtils.log(msg: "remote user join: \(uid) \(elapsed)ms", level: .info) + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) // Only one remote video view is available for this // tutorial. Here we check if there exists a surface @@ -152,7 +177,7 @@ extension JoinChannelMain: AgoraRtcEngineDelegate { /// @param reason reason why this user left, note this event may be triggered when the remote user /// become an audience in live broadcasting profile func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { - LogUtils.log(msg: "remote user left: \(uid) reason \(reason)", level: .info) + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) // to unlink your view from sdk, so that your view reference will be released // note the video will stay at its last frame, to completely remove it diff --git a/iOS/APIExample/Examples/Advanced/CustomVideoSourceMediaIO/zh-Hans.lproj/CustomVideoSourceMediaIO.strings b/iOS/APIExample/Examples/Advanced/CustomVideoSourceMediaIO/zh-Hans.lproj/CustomVideoSourceMediaIO.strings new file mode 100644 index 000000000..e7000e593 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/CustomVideoSourceMediaIO/zh-Hans.lproj/CustomVideoSourceMediaIO.strings @@ -0,0 +1,12 @@ + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UIViewController"; title = "Join Channel Video"; ObjectID = "fUU-7w-Gps"; */ +"fUU-7w-Gps.title" = "视频自采集(MediaIO)"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; + +/* Class = "UINavigationItem"; title = "Join Channel"; ObjectID = "wTi-y2-w4x"; */ +"wTi-y2-w4x.title" = "Join Channel"; diff --git a/iOS/APIExample/Examples/Advanced/CustomVideoSourcePush/Base.lproj/CustomVideoSourcePush.storyboard b/iOS/APIExample/Examples/Advanced/CustomVideoSourcePush/Base.lproj/CustomVideoSourcePush.storyboard new file mode 100644 index 000000000..61d404542 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/CustomVideoSourcePush/Base.lproj/CustomVideoSourcePush.storyboard @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/CustomVideoSourcePush/CustomVideoSourcePush.swift b/iOS/APIExample/Examples/Advanced/CustomVideoSourcePush/CustomVideoSourcePush.swift new file mode 100644 index 000000000..5a127f784 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/CustomVideoSourcePush/CustomVideoSourcePush.swift @@ -0,0 +1,228 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import UIKit +import AGEVideoLayout +import AgoraRtcKit + +class CustomVideoSourcePreview : UIView { + private var previewLayer: AVCaptureVideoPreviewLayer? + + func insertCaptureVideoPreviewLayer(previewLayer: AVCaptureVideoPreviewLayer) { + self.previewLayer?.removeFromSuperlayer() + + previewLayer.frame = bounds + layer.insertSublayer(previewLayer, below: layer.sublayers?.first) + self.previewLayer = previewLayer + } + + override func layoutSublayers(of layer: CALayer) { + super.layoutSublayers(of: layer) + previewLayer?.frame = bounds + } +} + +class CustomVideoSourcePushEntry : UIViewController +{ + @IBOutlet weak var joinButton: AGButton! + @IBOutlet weak var channelTextField: AGTextField! + let identifier = "CustomVideoSourcePush" + + override func viewDidLoad() { + super.viewDidLoad() + } + + @IBAction func doJoinPressed(sender: AGButton) { + guard let channelName = channelTextField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName":channelName] + self.navigationController?.pushViewController(newViewController, animated: true) + } +} + +class CustomVideoSourcePushMain: BaseViewController { + var localVideo = CustomVideoSourcePreview(frame: CGRect.zero) + var remoteVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + var customCamera:AgoraCameraSourcePush? + + @IBOutlet weak var container: AGEVideoContainer! + var agoraKit: AgoraRtcEngineKit! + + // indicate if current instance has joined channel + var isJoined: Bool = false + + override func viewDidLoad() { + super.viewDidLoad() + // layout render view + remoteVideo.setPlaceholder(text: "Remote Host".localized) + container.layoutStream(views: [localVideo, remoteVideo]) + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + // get channel name from configs + guard let channelName = configs["channelName"] as? String, + let resolution = GlobalSettings.shared.getSetting(key: "resolution")?.selectedOption().value as? CGSize, + let fps = GlobalSettings.shared.getSetting(key: "fps")?.selectedOption().value as? AgoraVideoFrameRate, + let orientation = GlobalSettings.shared.getSetting(key: "orientation")?.selectedOption().value as? AgoraVideoOutputOrientationMode else {return} + + // make myself a broadcaster + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) + + // enable video module and set up video encoding configs + agoraKit.enableVideo() + + // setup my own camera as custom video source + // note setupLocalVideo is not working when using pushExternalVideoFrame + // so you will have to prepare the preview yourself + customCamera = AgoraCameraSourcePush(delegate: self, videoView:localVideo) + agoraKit.setExternalVideoSource(true, useTexture: true, pushMode: true) + customCamera?.startCapture(ofCamera: .defaultCamera()) + + + agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: resolution, + frameRate: fps, + bitrate: AgoraVideoBitrateStandard, + orientationMode: orientation)) + + + + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channelName, info: nil, uid: 0, options: option) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // stop capture + customCamera?.stopCapture() + // leave channel when exiting the view + if isJoined { + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + } + } +} + +/// agora rtc engine delegate events +extension CustomVideoSourcePushMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // Only one remote video view is available for this + // tutorial. Here we check if there exists a surface + // view tagged as this uid. + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + } +} + +/// agora camera video source, the delegate will get frame data from camera +extension CustomVideoSourcePushMain:AgoraCameraSourcePushDelegate +{ + func myVideoCapture(_ capture: AgoraCameraSourcePush, didOutputSampleBuffer pixelBuffer: CVPixelBuffer, rotation: Int, timeStamp: CMTime) { + let videoFrame = AgoraVideoFrame() + videoFrame.format = 12 + videoFrame.textureBuf = pixelBuffer + videoFrame.time = timeStamp + videoFrame.rotation = Int32(rotation) + + //once we have the video frame, we can push to agora sdk + agoraKit?.pushExternalVideoFrame(videoFrame) + } + + +} diff --git a/iOS/APIExample/Examples/Advanced/CustomVideoSourcePush/zh-Hans.lproj/CustomVideoSourcePush.strings b/iOS/APIExample/Examples/Advanced/CustomVideoSourcePush/zh-Hans.lproj/CustomVideoSourcePush.strings new file mode 100644 index 000000000..40d7b4995 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/CustomVideoSourcePush/zh-Hans.lproj/CustomVideoSourcePush.strings @@ -0,0 +1,12 @@ + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UINavigationItem"; title = "Join Channel"; ObjectID = "a4k-1t-KLv"; */ +"a4k-1t-KLv.title" = "Join Channel"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; + +/* Class = "UIViewController"; title = "Join Channel Video"; ObjectID = "pjq-Wh-4Ys"; */ +"pjq-Wh-4Ys.title" = "视频自采集(Push)"; diff --git a/iOS/APIExample/Examples/Advanced/JoinMultiChannel/Base.lproj/JoinMultiChannel.storyboard b/iOS/APIExample/Examples/Advanced/JoinMultiChannel/Base.lproj/JoinMultiChannel.storyboard new file mode 100644 index 000000000..43ba36d23 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/JoinMultiChannel/Base.lproj/JoinMultiChannel.storyboard @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/JoinMultiChannel/JoinMultiChannel.swift b/iOS/APIExample/Examples/Advanced/JoinMultiChannel/JoinMultiChannel.swift new file mode 100644 index 000000000..db9cde873 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/JoinMultiChannel/JoinMultiChannel.swift @@ -0,0 +1,244 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import UIKit +import AGEVideoLayout +import AgoraRtcKit + +class JoinMultiChannelEntry : UIViewController +{ + @IBOutlet weak var joinButton: AGButton! + @IBOutlet weak var channelTextField: AGTextField! + let identifier = "JoinMultiChannel" + + override func viewDidLoad() { + super.viewDidLoad() + } + + @IBAction func doJoinPressed(sender: AGButton) { + guard let channelName = channelTextField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName":channelName] + self.navigationController?.pushViewController(newViewController, animated: true) + } +} + +class JoinMultiChannelMain: BaseViewController { + var localVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + var channel1RemoteVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + var channel2RemoteVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + + @IBOutlet weak var container1: AGEVideoContainer! + @IBOutlet weak var container2: AGEVideoContainer! + @IBOutlet weak var label1: UILabel! + @IBOutlet weak var label2: UILabel! + var channel1: AgoraRtcChannel? + var channel2: AgoraRtcChannel? + var agoraKit: AgoraRtcEngineKit! + + // indicate if current instance has joined channel + var isJoined1: Bool = false + var isJoined2: Bool = false + + override func viewDidLoad() { + super.viewDidLoad() + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + // get channel name from configs + guard let channelName = configs["channelName"] as? String, + let resolution = GlobalSettings.shared.getSetting(key: "resolution")?.selectedOption().value as? CGSize, + let fps = GlobalSettings.shared.getSetting(key: "fps")?.selectedOption().value as? AgoraVideoFrameRate, + let orientation = GlobalSettings.shared.getSetting(key: "orientation")?.selectedOption().value as? AgoraVideoOutputOrientationMode else {return} + let channelName1 = "\(channelName)" + let channelName2 = "\(channelName)-2" + + // layout render view + localVideo.setPlaceholder(text: "Local Host".localized) + channel1RemoteVideo.setPlaceholder(text: "\(channelName1)\nRemote Host") + channel2RemoteVideo.setPlaceholder(text: "\(channelName2)\nRemote Host") + container1.layoutStream(views: [localVideo, channel1RemoteVideo]) + container2.layoutStream(views: [channel2RemoteVideo]) + + // enable video module and set up video encoding configs + agoraKit.enableVideo() + agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: resolution, + frameRate: fps, + bitrate: AgoraVideoBitrateStandard, + orientationMode: orientation)) + + // set live broadcaster to send stream + agoraKit.setChannelProfile(.liveBroadcasting) + + // set up local video to render your local camera preview + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + // the view to be binded + videoCanvas.view = localVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + agoraKit.startPreview() + + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + + // auto subscribe options after join channel + let mediaOptions = AgoraRtcChannelMediaOptions() + mediaOptions.autoSubscribeAudio = true + mediaOptions.autoSubscribeVideo = true + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + channel1 = agoraKit.createRtcChannel(channelName1) + channel1?.setClientRole(.broadcaster) + label1.text = channelName1 + channel1?.setRtcChannelDelegate(self) + // a channel will only upstream video if you call publish + // there can be only 1 channel upstreaming at the same time, but you can have multiple channel downstreaming + channel1?.publish() + var result = channel1?.join(byToken: nil, info: nil, uid: 0, options: mediaOptions) ?? -1 + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel1 call failed: \(result), please check your params") + } + + channel2 = agoraKit.createRtcChannel(channelName2) + label2.text = channelName2 + channel2?.setRtcChannelDelegate(self) + result = channel2?.join(byToken: nil, info: nil, uid: 0, options: mediaOptions) ?? -1 + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel2 call failed: \(result), please check your params") + } + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + channel1?.leave() + channel1?.destroy() + channel2?.leave() + channel2?.destroy() + } + } +} + +/// agora rtc engine delegate events +extension JoinMultiChannelMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } +} + +extension JoinMultiChannelMain: AgoraRtcChannelDelegate +{ + func rtcChannelDidJoin(_ rtcChannel: AgoraRtcChannel, withUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "Join \(rtcChannel.getId() ?? "") with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + /// callback when warning occured for a channel, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcChannel(_ rtcChannel: AgoraRtcChannel, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "channel: \(rtcChannel.getId() ?? ""), warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for a channel, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcChannel(_ rtcChannel: AgoraRtcChannel, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcChannel(_ rtcChannel: AgoraRtcChannel, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // Only one remote video view is available for this + // tutorial. Here we check if there exists a surface + // view tagged as this uid. + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = channel1 == rtcChannel ? channel1RemoteVideo.videoView : channel2RemoteVideo.videoView + videoCanvas.renderMode = .hidden + // set channelId so that it knows which channel the video belongs to + videoCanvas.channelId = rtcChannel.getId() + agoraKit.setupRemoteVideo(videoCanvas) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcChannel(_ rtcChannel: AgoraRtcChannel, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + // set channelId so that it knows which channel the video belongs to + videoCanvas.channelId = rtcChannel.getId() + agoraKit.setupRemoteVideo(videoCanvas) + } +} diff --git a/iOS/APIExample/Examples/Advanced/JoinMultiChannel/zh-Hans.lproj/JoinMultiChannel.strings b/iOS/APIExample/Examples/Advanced/JoinMultiChannel/zh-Hans.lproj/JoinMultiChannel.strings new file mode 100644 index 000000000..ea06f7e53 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/JoinMultiChannel/zh-Hans.lproj/JoinMultiChannel.strings @@ -0,0 +1,12 @@ + +/* Class = "UIViewController"; title = "Join Channel Video"; ObjectID = "4JZ-MT-fZb"; */ +"4JZ-MT-fZb.title" = "加入多频道"; + +/* Class = "UINavigationItem"; title = "Join Channel"; ObjectID = "BpR-ES-aVX"; */ +"BpR-ES-aVX.title" = "Join Channel"; + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; diff --git a/iOS/APIExample/Examples/Advanced/LiveStreaming/Base.lproj/LiveStreaming.storyboard b/iOS/APIExample/Examples/Advanced/LiveStreaming/Base.lproj/LiveStreaming.storyboard new file mode 100644 index 000000000..91a16164e --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/LiveStreaming/Base.lproj/LiveStreaming.storyboard @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/LiveStreaming/LiveStreaming.swift b/iOS/APIExample/Examples/Advanced/LiveStreaming/LiveStreaming.swift new file mode 100644 index 000000000..02fbf6774 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/LiveStreaming/LiveStreaming.swift @@ -0,0 +1,323 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import UIKit +import AGEVideoLayout +import AgoraRtcKit + +class LiveStreamingEntry : UIViewController +{ + @IBOutlet weak var joinButton: UIButton! + @IBOutlet weak var channelTextField: UITextField! + let identifier = "LiveStreaming" + var role:AgoraClientRole = .broadcaster + + override func viewDidLoad() { + super.viewDidLoad() + } + + func getRoleAction(_ role: AgoraClientRole) -> UIAlertAction{ + return UIAlertAction(title: "\(role.description())", style: .default, handler: {[unowned self] action in + self.role = role + self.doJoin() + }) + } + + + @IBAction func doJoinPressed(sender: UIButton) { + guard let _ = channelTextField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() + + //display role picker + let alert = UIAlertController(title: "Pick Role".localized, message: nil, preferredStyle: .actionSheet) + alert.addAction(getRoleAction(.broadcaster)) + alert.addAction(getRoleAction(.audience)) + alert.addCancelAction() + present(alert, animated: true, completion: nil) + } + + func doJoin() { + guard let channelName = channelTextField.text else {return} + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName":channelName, "role":self.role] + self.navigationController?.pushViewController(newViewController, animated: true) + } +} + +class LiveStreamingMain: BaseViewController { + var foregroundVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + var backgroundVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + @IBOutlet weak var foregroundVideoContainer:UIView! + @IBOutlet weak var backgroundVideoContainer:UIView! + @IBOutlet weak var clientRoleToggleView:UIView! + @IBOutlet weak var ultraLowLatencyToggleView:UIView! + @IBOutlet weak var clientRoleToggle:UISwitch! + @IBOutlet weak var ultraLowLatencyToggle:UISwitch! + var remoteUid: UInt? { + didSet { + foregroundVideoContainer.isHidden = !(role == .broadcaster && remoteUid != nil) + } + } + var agoraKit: AgoraRtcEngineKit! + var role: AgoraClientRole = .broadcaster { + didSet { + foregroundVideoContainer.isHidden = !(role == .broadcaster && remoteUid != nil) + ultraLowLatencyToggle.isEnabled = role == .audience + } + } + var isLocalVideoForeground = false { + didSet { + if isLocalVideoForeground { + foregroundVideo.setPlaceholder(text: "Local Host".localized) + backgroundVideo.setPlaceholder(text: "Remote Host".localized) + } else { + foregroundVideo.setPlaceholder(text: "Remote Host".localized) + backgroundVideo.setPlaceholder(text: "Local Host".localized) + } + } + } + var isUltraLowLatencyOn: Bool = false + + // indicate if current instance has joined channel + var isJoined: Bool = false + + override func viewDidLoad() { + super.viewDidLoad() + + // layout render view + foregroundVideoContainer.addSubview(foregroundVideo) + backgroundVideoContainer.addSubview(backgroundVideo) + foregroundVideo.bindFrameToSuperviewBounds() + backgroundVideo.bindFrameToSuperviewBounds() + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + // get channel name from configs + guard let channelName = configs["channelName"] as? String, + let role = configs["role"] as? AgoraClientRole else {return} + + // for audience put local video in foreground + isLocalVideoForeground = role == .audience + // if inital role is broadcaster, do not show audience options + clientRoleToggleView.isHidden = role == .broadcaster + ultraLowLatencyToggleView.isHidden = role == .broadcaster + + // make this room live broadcasting room + agoraKit.setChannelProfile(.liveBroadcasting) + updateClientRole(role) + + // enable video module and set up video encoding configs + agoraKit.enableVideo() + + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: nil, channelId: channelName, info: nil, uid: 0, options: option) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + /// make myself a broadcaster + func becomeBroadcaster() { + guard let resolution = GlobalSettings.shared.getSetting(key: "resolution")?.selectedOption().value as? CGSize, + let fps = GlobalSettings.shared.getSetting(key: "fps")?.selectedOption().value as? AgoraVideoFrameRate, + let orientation = GlobalSettings.shared.getSetting(key: "orientation")?.selectedOption().value as? AgoraVideoOutputOrientationMode else { + LogUtils.log(message: "invalid video configurations, failed to become broadcaster", level: .error) + return + } + agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: resolution, + frameRate: fps, + bitrate: AgoraVideoBitrateStandard, + orientationMode: orientation)) + + // set up local video to render your local camera preview + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + // the view to be binded + videoCanvas.view = localVideoCanvas() + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + + // enable camera/mic, this will bring up permission dialog for first time + agoraKit.enableLocalVideo(true) + agoraKit.enableLocalAudio(true) + + agoraKit.setClientRole(.broadcaster, options: nil) + } + + /// make myself an audience + func becomeAudience() { + // unbind view + agoraKit.setupLocalVideo(nil) + // You have to provide client role options if set to audience + let options = AgoraClientRoleOptions() + options.audienceLatencyLevel = isUltraLowLatencyOn ? .ultraLowLatency : .lowLatency + agoraKit.setClientRole(.audience, options: options) + } + + func localVideoCanvas() -> UIView { + return isLocalVideoForeground ? foregroundVideo.videoView : backgroundVideo.videoView + } + + func remoteVideoCanvas() -> UIView { + return isLocalVideoForeground ? backgroundVideo.videoView : foregroundVideo.videoView + } + + @IBAction func onTapForegroundVideo(_ sender:UIGestureRecognizer) { + isLocalVideoForeground = !isLocalVideoForeground + + let localVideoCanvas = AgoraRtcVideoCanvas() + localVideoCanvas.uid = 0 + localVideoCanvas.renderMode = .hidden + localVideoCanvas.view = self.localVideoCanvas() + + let remoteVideoCanvas = AgoraRtcVideoCanvas() + remoteVideoCanvas.renderMode = .hidden + remoteVideoCanvas.view = self.remoteVideoCanvas() + + agoraKit.setupLocalVideo(localVideoCanvas) + if let uid = remoteUid { + remoteVideoCanvas.uid = uid + agoraKit.setupRemoteVideo(remoteVideoCanvas) + } + } + + @IBAction func onToggleClientRole(_ sender:UISwitch) { + let role:AgoraClientRole = sender.isOn ? .broadcaster : .audience + updateClientRole(role) + } + + fileprivate func updateClientRole(_ role:AgoraClientRole) { + self.role = role + if(role == .broadcaster) { + becomeBroadcaster() + } else { + becomeAudience() + } + } + + @IBAction func onToggleUltraLowLatency(_ sender:UISwitch) { + updateUltraLowLatency(sender.isOn) + } + + fileprivate func updateUltraLowLatency(_ enabled:Bool) { + if(self.role == .audience) { + self.isUltraLowLatencyOn = enabled + updateClientRole(.audience) + } + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + // deregister packet processing + AgoraCustomEncryption.deregisterPacketProcessing(agoraKit) + if isJoined { + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + } + } +} + +/// agora rtc engine delegate events +extension LiveStreamingMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + //record remote uid + remoteUid = uid + + // Only one remote video view is available for this + // tutorial. Here we check if there exists a surface + // view tagged as this uid. + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideoCanvas() + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + //clear remote uid + if(remoteUid == uid){ + remoteUid = nil + } + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + } +} diff --git a/iOS/APIExample/Examples/Advanced/LiveStreaming/zh-Hans.lproj/LiveStreaming.strings b/iOS/APIExample/Examples/Advanced/LiveStreaming/zh-Hans.lproj/LiveStreaming.strings new file mode 100644 index 000000000..dbfe54007 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/LiveStreaming/zh-Hans.lproj/LiveStreaming.strings @@ -0,0 +1,12 @@ + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UILabel"; text = "Ultra Low Latency"; ObjectID = "Lzz-2R-G7f"; */ +"Lzz-2R-G7f.text" = "极速直播"; + +/* Class = "UILabel"; text = "Co-host"; ObjectID = "XcJ-am-UAb"; */ +"XcJ-am-UAb.text" = "连麦"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; diff --git a/iOS/APIExample/Examples/Advanced/MediaChannelRelay/Base.lproj/MediaChannelRelay.storyboard b/iOS/APIExample/Examples/Advanced/MediaChannelRelay/Base.lproj/MediaChannelRelay.storyboard new file mode 100644 index 000000000..bc59bc14a --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/MediaChannelRelay/Base.lproj/MediaChannelRelay.storyboard @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/MediaChannelRelay/MediaChannelRelay.swift b/iOS/APIExample/Examples/Advanced/MediaChannelRelay/MediaChannelRelay.swift new file mode 100644 index 000000000..b5ff13d84 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/MediaChannelRelay/MediaChannelRelay.swift @@ -0,0 +1,254 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import UIKit +import AGEVideoLayout +import AgoraRtcKit + +class MediaChannelRelayEntry : UIViewController +{ + @IBOutlet weak var joinButton: UIButton! + @IBOutlet weak var channelTextField: UITextField! + let identifier = "MediaChannelRelay" + + override func viewDidLoad() { + super.viewDidLoad() + } + + @IBAction func doJoinPressed(sender: UIButton) { + guard let channelName = channelTextField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName":channelName] + self.navigationController?.pushViewController(newViewController, animated: true) + } +} + +class MediaChannelRelayMain: BaseViewController { + var localVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + var remoteVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + + @IBOutlet weak var container: AGEVideoContainer! + @IBOutlet weak var relayButton: UIButton! + @IBOutlet weak var stopButton: UIButton! + @IBOutlet weak var relayChannelField: UITextField! + var agoraKit: AgoraRtcEngineKit! + + // indicate if current instance has joined channel + var isJoined: Bool = false + var isRelaying: Bool = false { + didSet { + stopButton.isHidden = !isRelaying + relayButton.isHidden = isRelaying + relayChannelField.isEnabled = !isRelaying + } + } + + override func viewDidLoad() { + super.viewDidLoad() + + // layout render view + localVideo.setPlaceholder(text: "Local Host".localized) + remoteVideo.setPlaceholder(text: "Remote Host".localized) + container.layoutStream(views: [localVideo, remoteVideo]) + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + // get channel name from configs + guard let channelName = configs["channelName"] as? String, + let resolution = GlobalSettings.shared.getSetting(key: "resolution")?.selectedOption().value as? CGSize, + let fps = GlobalSettings.shared.getSetting(key: "fps")?.selectedOption().value as? AgoraVideoFrameRate, + let orientation = GlobalSettings.shared.getSetting(key: "orientation")?.selectedOption().value as? AgoraVideoOutputOrientationMode else {return} + + + // make myself a broadcaster + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) + + // enable video module and set up video encoding configs + agoraKit.enableVideo() + agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: resolution, + frameRate: fps, + bitrate: AgoraVideoBitrateStandard, + orientationMode: orientation)) + + // set up local video to render your local camera preview + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + // the view to be binded + videoCanvas.view = localVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channelName, info: nil, uid: SCREEN_SHARE_BROADCASTER_UID, options: option) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + /// start relay + @IBAction func doRelay(_ sender: UIButton) { + guard let destinationChannelName = relayChannelField.text else {return} + + // prevent operation if target channel name is empty + if(destinationChannelName.isEmpty) { + self.showAlert(message: "Destination channel name is empty") + return + } + + // configure source info, channel name defaults to current, and uid defaults to local + let config = AgoraChannelMediaRelayConfiguration() + config.sourceInfo = AgoraChannelMediaRelayInfo(token: nil) + + // configure target channel info + let destinationInfo = AgoraChannelMediaRelayInfo(token: nil) + config.setDestinationInfo(destinationInfo, forChannelName: destinationChannelName) + agoraKit.startChannelMediaRelay(config) + } + + /// stop relay + @IBAction func doStop(_ sender: UIButton) { + agoraKit.stopChannelMediaRelay() + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + // deregister packet processing + AgoraCustomEncryption.deregisterPacketProcessing(agoraKit) + if isJoined { + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + } + } +} + +/// agora rtc engine delegate events +extension MediaChannelRelayMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // Only one remote video view is available for this + // tutorial. Here we check if there exists a surface + // view tagged as this uid. + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + } + + /// callback when a media relay process state changed + /// @param state state of media relay + /// @param error error details if media relay reaches failure state + func rtcEngine(_ engine: AgoraRtcEngineKit, channelMediaRelayStateDidChange state: AgoraChannelMediaRelayState, error: AgoraChannelMediaRelayError) { + LogUtils.log(message: "channelMediaRelayStateDidChange: \(state.rawValue) error \(error.rawValue)", level: .info) + + switch(state){ + case .running: + isRelaying = true + break + case .failure: + showAlert(message: "Media Relay Failed: \(error.rawValue)") + isRelaying = false + break + case .idle: + isRelaying = false + break + default:break + } + } + + /// callback when a media relay event received + /// @param event event of media relay + func rtcEngine(_ engine: AgoraRtcEngineKit, didReceive event: AgoraChannelMediaRelayEvent) { + LogUtils.log(message: "didReceiveRelayEvent: \(event.rawValue)", level: .info) + } +} diff --git a/iOS/APIExample/Examples/Advanced/MediaChannelRelay/zh-Hans.lproj/MediaChannelRelay.strings b/iOS/APIExample/Examples/Advanced/MediaChannelRelay/zh-Hans.lproj/MediaChannelRelay.strings new file mode 100644 index 000000000..db6fd316c --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/MediaChannelRelay/zh-Hans.lproj/MediaChannelRelay.strings @@ -0,0 +1,21 @@ + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UIButton"; normalTitle = "Stop"; ObjectID = "Kw7-C4-nP2"; */ +"Kw7-C4-nP2.normalTitle" = "停止"; + +/* Class = "UITextField"; placeholder = "Enter target relay channel name"; ObjectID = "aLa-HX-eD8"; */ +"aLa-HX-eD8.placeholder" = "输入流转发目标频道名"; + +/* Class = "UIViewController"; title = "Join Channel Audio"; ObjectID = "jxp-ZN-2yG"; */ +"jxp-ZN-2yG.title" = "Join Channel Audio"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; + +/* Class = "UIButton"; normalTitle = "Relay"; ObjectID = "sK1-s8-Hpa"; */ +"sK1-s8-Hpa.normalTitle" = "转发"; + +/* Class = "UILabel"; text = "Send stream to another channel"; ObjectID = "sNN-B3-EH6"; */ +"sNN-B3-EH6.text" = "发送流到另一个频道"; diff --git a/iOS/APIExample/Examples/Advanced/MediaPlayer/Base.lproj/MediaPlayer.storyboard b/iOS/APIExample/Examples/Advanced/MediaPlayer/Base.lproj/MediaPlayer.storyboard new file mode 100644 index 000000000..fb1c648f5 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/MediaPlayer/Base.lproj/MediaPlayer.storyboard @@ -0,0 +1,214 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/MediaPlayer/MediaPlayer.swift b/iOS/APIExample/Examples/Advanced/MediaPlayer/MediaPlayer.swift new file mode 100644 index 000000000..57de2f6f5 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/MediaPlayer/MediaPlayer.swift @@ -0,0 +1,303 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import UIKit +import AGEVideoLayout +import AgoraRtcKit +import AgoraMediaPlayer + +class MediaPlayerEntry : UIViewController +{ + @IBOutlet weak var joinButton: UIButton! + @IBOutlet weak var channelTextField: UITextField! + let identifier = "MediaPlayer" + + override func viewDidLoad() { + super.viewDidLoad() + } + + @IBAction func doJoinPressed(sender: UIButton) { + guard let channelName = channelTextField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName":channelName] + self.navigationController?.pushViewController(newViewController, animated: true) + } + +} + +class MediaPlayerMain: BaseViewController { + var localVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + var remoteVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + + @IBOutlet weak var container: AGEVideoContainer! + @IBOutlet weak var mediaUrlField: UITextField! + @IBOutlet weak var playerControlStack: UIStackView! + @IBOutlet weak var playerProgressSlider: UISlider! + @IBOutlet weak var playerVolumeSlider: UISlider! + @IBOutlet weak var playerDurationLabel: UILabel! + var agoraKit: AgoraRtcEngineKit! + var mediaPlayerKit: AgoraMediaPlayer! + var timer:Timer? + + // indicate if current instance has joined channel + var isJoined: Bool = false + + override func viewDidLoad() { + super.viewDidLoad() + // layout render view + localVideo.setPlaceholder(text: "No Player Loaded") + remoteVideo.setPlaceholder(text: "Remote Host".localized) + container.layoutStream1x2(views: [localVideo, remoteVideo]) + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + // get channel name from configs + guard let channelName = configs["channelName"] as? String, + let resolution = GlobalSettings.shared.getSetting(key: "resolution")?.selectedOption().value as? CGSize, + let fps = GlobalSettings.shared.getSetting(key: "fps")?.selectedOption().value as? AgoraVideoFrameRate, + let orientation = GlobalSettings.shared.getSetting(key: "orientation")?.selectedOption().value as? AgoraVideoOutputOrientationMode else {return} + + // become a live broadcaster + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) + + // enable video module and set up video encoding configs + agoraKit.enableAudio() + agoraKit.enableVideo() + agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: resolution, + frameRate: fps, + bitrate: AgoraVideoBitrateStandard, + orientationMode: orientation)) + + // prepare media player + mediaPlayerKit = AgoraMediaPlayer(delegate: self) + // attach player to agora rtc kit, so that the media stream can be published + AgoraRtcChannelPublishHelper.shareInstance().attachPlayer(toRtc: mediaPlayerKit, rtcEngine: agoraKit, enableVideoSource: true) + AgoraRtcChannelPublishHelper.shareInstance().register(self) + + // set media local play view + mediaPlayerKit.setView(localVideo.videoView) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channelName, info: nil, uid: 0, options: option) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + @IBAction func doOpenMediaUrl(sender: UIButton) { + guard let url = mediaUrlField.text else {return} + //resign text field + mediaUrlField.resignFirstResponder() + + mediaPlayerKit.open(url, startPos: 0) + } + + @IBAction func doPlay(sender: UIButton) { + mediaPlayerKit.play() + } + + @IBAction func doStop(sender: UIButton) { + mediaPlayerKit.stop() + } + + @IBAction func doPause(sender: UIButton) { + mediaPlayerKit.pause() + } + + @IBAction func doPublish(sender: UIButton) { + AgoraRtcChannelPublishHelper.shareInstance().publishVideo() + AgoraRtcChannelPublishHelper.shareInstance().publishAudio() + } + + @IBAction func doUnpublish(sender: UIButton) { + AgoraRtcChannelPublishHelper.shareInstance().unpublishVideo() + AgoraRtcChannelPublishHelper.shareInstance().unpublishAudio() + } + + @IBAction func doSeek(sender: UISlider) { + mediaPlayerKit.seek(toPosition: Int(sender.value * Float(mediaPlayerKit.getDuration()))) + } + + @IBAction func doAdjustPlayoutVolume(sender: UISlider) { + AgoraRtcChannelPublishHelper.shareInstance().adjustPlayoutSignalVolume(Int32(Int(sender.value))) + } + + @IBAction func doAdjustPublishVolume(sender: UISlider) { + AgoraRtcChannelPublishHelper.shareInstance().adjustPublishSignalVolume(Int32(Int(sender.value))) + } + + func startProgressTimer() { + // begin timer to update progress + if(timer == nil) { + timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self](timer:Timer) in + guard let weakself = self else {return} + let progress = Float(weakself.mediaPlayerKit.getPlayPosition()) / Float(weakself.mediaPlayerKit.getDuration()) + if(!weakself.playerProgressSlider.isTouchInside) { + weakself.playerProgressSlider.setValue(progress, animated: true) + } + }) + } + } + + func stopProgressTimer() { + // stop timer + if(timer != nil) { + timer?.invalidate() + timer = nil + } + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + // deregister packet processing + AgoraCustomEncryption.deregisterPacketProcessing(agoraKit) + if isJoined { + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + } + } +} + +/// agora rtc engine delegate events +extension MediaPlayerMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // Only one remote video view is available for this + // tutorial. Here we check if there exists a surface + // view tagged as this uid. + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + } +} + +extension MediaPlayerMain: AgoraMediaPlayerDelegate +{ + +} + +extension MediaPlayerMain: AgoraRtcChannelPublishHelperDelegate +{ + func agoraRtcChannelPublishHelperDelegate(_ playerKit: AgoraMediaPlayer, didChangedTo state: AgoraMediaPlayerState, error: AgoraMediaPlayerError) { + LogUtils.log(message: "player rtc channel publish helper state changed to: \(state.rawValue), error: \(error.rawValue)", level: .info) + + DispatchQueue.main.async {[weak self] in + guard let weakself = self else {return} + switch state { + case .failed: + weakself.showAlert(message: "media player error: \(error.rawValue)") + break + case .openCompleted: + let duration = weakself.mediaPlayerKit.getDuration() + weakself.playerControlStack.isHidden = false + weakself.playerDurationLabel.text = "\(String(format: "%02d", duration / 60)) : \(String(format: "%02d", duration % 60))" + break + case .stopped: + weakself.playerControlStack.isHidden = true + weakself.stopProgressTimer() + break + case .idle: break + case .opening: break + case .playing: + weakself.startProgressTimer() + break + case .paused: + weakself.stopProgressTimer() + break; + case .playBackCompleted: + weakself.stopProgressTimer() + break + default: break + } + } + } +} diff --git a/iOS/APIExample/Examples/Advanced/MediaPlayer/zh-Hans.lproj/MediaPlayer.strings b/iOS/APIExample/Examples/Advanced/MediaPlayer/zh-Hans.lproj/MediaPlayer.strings new file mode 100644 index 000000000..f73f63455 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/MediaPlayer/zh-Hans.lproj/MediaPlayer.strings @@ -0,0 +1,39 @@ + +/* Class = "UILabel"; text = "00 : 00"; ObjectID = "4et-fL-YHJ"; */ +"4et-fL-YHJ.text" = "00 : 00"; + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UIButton"; normalTitle = "Publish"; ObjectID = "Leb-Wc-wyE"; */ +"Leb-Wc-wyE.normalTitle" = "发流"; + +/* Class = "UIButton"; normalTitle = "Open"; ObjectID = "bBH-Cp-zvD"; */ +"bBH-Cp-zvD.normalTitle" = "打开"; + +/* Class = "UIButton"; normalTitle = "Pause"; ObjectID = "gpl-j7-fNe"; */ +"gpl-j7-fNe.normalTitle" = "暂停"; + +/* Class = "UIButton"; normalTitle = "Unpublish"; ObjectID = "grZ-Qq-vYc"; */ +"grZ-Qq-vYc.normalTitle" = "停止发流"; + +/* Class = "UITextField"; text = "https://webdemo.agora.io/agora-web-showcase/examples/Agora-Custom-VideoSource-Web/assets/sample.mp4"; ObjectID = "jtM-0I-8yU"; */ +"jtM-0I-8yU.text" = "https://webdemo.agora.io/agora-web-showcase/examples/Agora-Custom-VideoSource-Web/assets/sample.mp4"; + +/* Class = "UIViewController"; title = "Join Channel Audio"; ObjectID = "jxp-ZN-2yG"; */ +"jxp-ZN-2yG.title" = "Join Channel Audio"; + +/* Class = "UILabel"; text = "Publish Volume"; ObjectID = "kIh-KH-AhZ"; */ +"kIh-KH-AhZ.text" = "发流音量"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; + +/* Class = "UILabel"; text = "Playout Volume"; ObjectID = "nDn-o2-Vmd"; */ +"nDn-o2-Vmd.text" = "播放音量"; + +/* Class = "UIButton"; normalTitle = "Stop"; ObjectID = "uBn-Om-6Vs"; */ +"uBn-Om-6Vs.normalTitle" = "停止"; + +/* Class = "UIButton"; normalTitle = "Play"; ObjectID = "vdv-zd-3aD"; */ +"vdv-zd-3aD.normalTitle" = "播放"; diff --git a/iOS/APIExample/Examples/Advanced/PrecallTest/Base.lproj/PrecallTest.storyboard b/iOS/APIExample/Examples/Advanced/PrecallTest/Base.lproj/PrecallTest.storyboard new file mode 100644 index 000000000..a5bcfd576 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/PrecallTest/Base.lproj/PrecallTest.storyboard @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/PrecallTest/PrecallTest.swift b/iOS/APIExample/Examples/Advanced/PrecallTest/PrecallTest.swift new file mode 100644 index 000000000..7bec922f2 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/PrecallTest/PrecallTest.swift @@ -0,0 +1,130 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import UIKit +import AGEVideoLayout +import AgoraRtcKit + +class PrecallTestEntry : UIViewController +{ + var agoraKit: AgoraRtcEngineKit! + var timer:Timer? + @IBOutlet weak var lastmileBtn: UIButton! + @IBOutlet weak var lastmileResultLabel: UILabel! + @IBOutlet weak var lastmileProbResultLabel: UILabel! + @IBOutlet weak var lastmileActivityView: UIActivityIndicatorView! + @IBOutlet weak var echoTestCountDownLabel: UILabel! + @IBOutlet weak var echoTestPopover: UIView! + @IBOutlet weak var echoValidateCountDownLabel: UILabel! + @IBOutlet weak var echoValidatePopover: UIView! + override func viewDidLoad() { + super.viewDidLoad() + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + // have to be a broadcaster for doing echo test + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) + } + + + @IBAction func doLastmileTest(sender: UIButton) { + lastmileActivityView.startAnimating() + let config = AgoraLastmileProbeConfig() + // do uplink testing + config.probeUplink = true; + // do downlink testing + config.probeDownlink = true; + // expected uplink bitrate, range: [100000, 5000000] + config.expectedUplinkBitrate = 100000; + // expected downlink bitrate, range: [100000, 5000000] + config.expectedDownlinkBitrate = 100000; + agoraKit.startLastmileProbeTest(config) + } + + @IBAction func doEchoTest(sender: UIButton) { + agoraKit.startEchoTest(withInterval: 10) + showPopover(isValidate: false, seconds: 10) {[unowned self] in + self.showPopover(isValidate: true, seconds: 10) {[unowned self] in + self.agoraKit.stopEchoTest() + } + } + } + + // show popover and hide after seconds + func showPopover(isValidate:Bool, seconds:Int, callback:@escaping (() -> Void)) { + var count = seconds + var countDownLabel:UILabel? + var popover:UIView? + if(isValidate) { + countDownLabel = echoValidateCountDownLabel + popover = echoValidatePopover + } else { + countDownLabel = echoTestCountDownLabel + popover = echoTestPopover + } + + countDownLabel?.text = "\(count)" + popover?.isHidden = false + timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {[unowned self] (timer) in + count -= 1 + countDownLabel?.text = "\(count)" + + if(count == 0) { + self.timer?.invalidate() + popover?.isHidden = true + callback() + } + } + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // clean up + // important, you will not be able to join a channel + // if you are in the middle of a testing + timer?.invalidate() + agoraKit.stopEchoTest() + agoraKit.stopLastmileProbeTest() + } + } +} + +extension PrecallTestEntry:AgoraRtcEngineDelegate +{ + /// callback to get lastmile quality 2seconds after startLastmileProbeTest + func rtcEngine(_ engine: AgoraRtcEngineKit, lastmileQuality quality: AgoraNetworkQuality) { + lastmileResultLabel.text = "Quality: \(quality.description())" + } + + /// callback to get more detail lastmile quality after startLastmileProbeTest + func rtcEngine(_ engine: AgoraRtcEngineKit, lastmileProbeTest result: AgoraLastmileProbeResult) { + let rtt = "Rtt: \(result.rtt)ms" + let downlinkBandwidth = "DownlinkAvailableBandwidth: \(result.downlinkReport.availableBandwidth)Kbps" + let downlinkJitter = "DownlinkJitter: \(result.downlinkReport.jitter)ms" + let downlinkLoss = "DownlinkLoss: \(result.downlinkReport.packetLossRate)%" + + let uplinkBandwidth = "UplinkAvailableBandwidth: \(result.uplinkReport.availableBandwidth)Kbps" + let uplinkJitter = "UplinkJitter: \(result.uplinkReport.jitter)ms" + let uplinkLoss = "UplinkLoss: \(result.uplinkReport.packetLossRate)%" + + lastmileProbResultLabel.text = [rtt, downlinkBandwidth, downlinkJitter, downlinkLoss, uplinkBandwidth, uplinkJitter, uplinkLoss].joined(separator: "\n") + + // stop testing after get last mile detail result + engine.stopLastmileProbeTest() + lastmileActivityView.stopAnimating() + } +} diff --git a/iOS/APIExample/Examples/Advanced/PrecallTest/zh-Hans.lproj/PrecallTest.strings b/iOS/APIExample/Examples/Advanced/PrecallTest/zh-Hans.lproj/PrecallTest.strings new file mode 100644 index 000000000..44c425bc8 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/PrecallTest/zh-Hans.lproj/PrecallTest.strings @@ -0,0 +1,24 @@ + +/* Class = "UILabel"; text = "Lastmile Network Pretest"; ObjectID = "3PN-IA-Upy"; */ +"3PN-IA-Upy.text" = "Lastmile 网络测试"; + +/* Class = "UILabel"; text = "10"; ObjectID = "4WV-kQ-0aJ"; */ +"4WV-kQ-0aJ.text" = "10"; + +/* Class = "UIButton"; normalTitle = "Start"; ObjectID = "CVA-Q1-OGl"; */ +"CVA-Q1-OGl.normalTitle" = "开始"; + +/* Class = "UILabel"; text = "Now you should hear what you said..."; ObjectID = "MdV-HB-V93"; */ +"MdV-HB-V93.text" = "现在你应该能听到前10秒的声音..."; + +/* Class = "UILabel"; text = "10"; ObjectID = "caY-D3-ysY"; */ +"caY-D3-ysY.text" = "10"; + +/* Class = "UILabel"; text = "Echo Pretest"; ObjectID = "e83-fp-COE"; */ +"e83-fp-COE.text" = "音频网络回路测试"; + +/* Class = "UIButton"; normalTitle = "Start"; ObjectID = "eol-rm-UUy"; */ +"eol-rm-UUy.normalTitle" = "开始"; + +/* Class = "UILabel"; text = "Please say something.."; ObjectID = "tFL-Md-flt"; */ +"tFL-Md-flt.text" = "尝试说一些话.."; diff --git a/iOS/APIExample/Examples/Advanced/QuickSwitchChannel/QuickSwitchChannel.swift b/iOS/APIExample/Examples/Advanced/QuickSwitchChannel/QuickSwitchChannel.swift new file mode 100644 index 000000000..b5d18621f --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/QuickSwitchChannel/QuickSwitchChannel.swift @@ -0,0 +1,281 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import UIKit +import AGEVideoLayout +import AgoraRtcKit + +struct ChannelInfo { + let channelName: String +} + +extension ChannelInfo { + /// static function to generate 4 channels based on given channel name + static func AllChannelList(_ channelName:String) -> [ChannelInfo] { + var channels = [ChannelInfo]() + for index in 1..<5 { + let channel = ChannelInfo( + channelName: "\(channelName)\(index)" + ) + channels.append(channel) + } + return channels + } +} + +class QuickSwitchChannel: BaseViewController { + var pageViewController: UIPageViewController! + var channels = [ChannelInfo]() + var currentIndex = 0 + var hostView: UIView? + var hostChannel: ChannelInfo? + var agoraKit: AgoraRtcEngineKit! + + // indicate if current instance has joined channel + var isJoined: Bool = false + + /// setup page controller, it will auto generates 4 channels with corresponding view controllers for you to swipe, every time you swipe to a new viewcontroller it will switch to that channel + func setupPageController(channelName: String) { + // generate all channel infos + channels = ChannelInfo.AllChannelList(channelName) + pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil) + pageViewController.delegate = self + + if let vc = channelViewController(at: currentIndex) { + pageViewController.setViewControllers([vc], direction: .forward, animated: false, completion: nil) + // there is only 1 rtc engine instance + // We will have to mark the current active view controller video renderer, so that we can use it in later didJoinedOfUser events + // setHostViewController grabs the hostRenderView from current active view controller, and store it in hostView property + setHostViewController(vc) + } + pageViewController.dataSource = self + + // add page view controller as child view controller + addChild(pageViewController) + view.addSubview(pageViewController.view) + pageViewController!.view.frame = view.bounds + pageViewController.didMove(toParent: self) + + // Add the page view controller's gesture recognizers to the view controller's view so that the gestures are started more easily. + view.gestureRecognizers = pageViewController.gestureRecognizers + } + + func setHostViewController(_ viewController: QuickSwitchChannelVCItem) { + hostChannel = viewController.channel + hostView = viewController.hostRenderView + // every time we switch a view controller, we will rejoin the channel by calling switchChannel, so we always call showLoading + viewController.showLoading(true) + } + + override func viewDidLoad() { + super.viewDidLoad() + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + // get channel name from configs + guard let channelName = configs["channelName"] as? String else {return} + + // setup UIPageController for swipe changing vc + setupPageController(channelName:channelName) + + // enable video module + agoraKit.enableVideo() + // make myself an audience + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.audience) + + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channels[currentIndex].channelName, info: nil, uid: 0, options: option) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + if isJoined { + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + } + } +} + +/// agora rtc engine delegate events +extension QuickSwitchChannel: AgoraRtcEngineDelegate { + /// callback when current user join channel successfully + /// @param channel channel name + /// @param uid my uid + /// @param elapsed elapsed time since joinChannel is called + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + self.isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + + // check if the callback should be responding + guard let currentVC = pageViewController.viewControllers?.first as? QuickSwitchChannelVCItem, + currentVC.channel.channelName == channel else { + return + } + + // hide loading + currentVC.showLoading(false) + } + + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // check if the callback should be responding + guard let currentVC = pageViewController.viewControllers?.first as? QuickSwitchChannelVCItem, + currentVC.channel.channelName == hostChannel?.channelName else { + return + } + + // Only one remote video view is available for this + // tutorial. Here we check if there exists a surface + // view tagged as this uid. + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = hostView + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // check if the callback should be responding + guard let currentVC = pageViewController.viewControllers?.first as? QuickSwitchChannelVCItem, + currentVC.channel.channelName == hostChannel?.channelName else { + return + } + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + } +} + +private extension QuickSwitchChannel { + /// api to generate QuickSwitchChannelVCItem on demand + func channelViewController(at index: Int) -> QuickSwitchChannelVCItem? { + guard channels.count > 0 else { + return nil + } + + var vcIndex = index + // if vcIndex exceeds maximum, reset to 0 + if vcIndex >= channels.count { + vcIndex = 0 + // else if vcIndex < 0, reset to last value + } else if vcIndex < 0 { + vcIndex = channels.count - 1 + } + + // create the view controller with info + let channelVC = QuickSwitchChannelVCItem(nibName: "QuickSwitchChannelVCItem", bundle: nil) + channelVC.index = vcIndex + channelVC.channel = channels[vcIndex] + + return channelVC + } +} + +/// Page View Controller DataSource +extension QuickSwitchChannel: UIPageViewControllerDataSource { + func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { + guard let channelVC = viewController as? QuickSwitchChannelVCItem else { + return nil + } + // return next VC + return channelViewController(at: channelVC.index + 1) + } + + func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { + guard let channelVC = viewController as? QuickSwitchChannelVCItem else { + return nil + } + // return prev VC + return channelViewController(at: channelVC.index - 1) + } +} + +/// Page View Controller Delegate +extension QuickSwitchChannel : UIPageViewControllerDelegate +{ + func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { + guard let previousVC = previousViewControllers.last as? QuickSwitchChannelVCItem, + let currentVC = pageViewController.viewControllers?.first as? QuickSwitchChannelVCItem, + previousVC != currentVC else { + return + } + + // switch to currentVC and its hosted channel + setHostViewController(currentVC) + let option = AgoraRtcChannelMediaOptions() + agoraKit.switchChannel(byToken: nil, channelId: currentVC.channel.channelName, options: option) + } +} diff --git a/iOS/APIExample/Examples/Advanced/QuickSwitchChannel/QuickSwitchChannelVCItem.swift b/iOS/APIExample/Examples/Advanced/QuickSwitchChannel/QuickSwitchChannelVCItem.swift new file mode 100644 index 000000000..02b6173e2 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/QuickSwitchChannel/QuickSwitchChannelVCItem.swift @@ -0,0 +1,39 @@ +// +// QuickSwitchChannelVCItem.swift +// Quick-Switch +// +// Created by GongYuhua on 2019/2/25. +// Copyright © 2019 Agora. All rights reserved. +// + +import UIKit + +class QuickSwitchChannelVCItem: UIViewController { + + @IBOutlet weak var hostLabel: UILabel! + @IBOutlet weak var channelLabel: UILabel! + @IBOutlet weak var spinner: UIActivityIndicatorView! + @IBOutlet weak var hostRenderView: UIView! + + var channel: ChannelInfo! + var index = 0 + + override func viewDidLoad() { + super.viewDidLoad() + updateView(with: channel) + } + + /// update channel info + func updateView(with channel: ChannelInfo) { + channelLabel.text = "Channel: \(channel.channelName)" + } + + /// show loading animation + func showLoading(_ shouldShow: Bool) { + if shouldShow { + spinner.startAnimating() + } else { + spinner.stopAnimating() + } + } +} diff --git a/iOS/APIExample/Examples/Advanced/QuickSwitchChannel/QuickSwitchChannelVCItem.xib b/iOS/APIExample/Examples/Advanced/QuickSwitchChannel/QuickSwitchChannelVCItem.xib new file mode 100644 index 000000000..b1cedf735 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/QuickSwitchChannel/QuickSwitchChannelVCItem.xib @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/RTMPInjection/Base.lproj/RTMPInjection.storyboard b/iOS/APIExample/Examples/Advanced/RTMPInjection/Base.lproj/RTMPInjection.storyboard new file mode 100644 index 000000000..e9656bd39 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/RTMPInjection/Base.lproj/RTMPInjection.storyboard @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Live-Broadcasting/RTMPInjection.swift b/iOS/APIExample/Examples/Advanced/RTMPInjection/RTMPInjection.swift similarity index 61% rename from iOS/APIExample/Examples/Live-Broadcasting/RTMPInjection.swift rename to iOS/APIExample/Examples/Advanced/RTMPInjection/RTMPInjection.swift index 5ed65fd43..d22993247 100644 --- a/iOS/APIExample/Examples/Live-Broadcasting/RTMPInjection.swift +++ b/iOS/APIExample/Examples/Advanced/RTMPInjection/RTMPInjection.swift @@ -1,5 +1,5 @@ // -// RTMPInjection.swift +// RTMPInjectionMain.swift // APIExample // // Created by CavanSu on 2020/4/30. @@ -10,23 +10,45 @@ import UIKit import AgoraRtcKit import AGEVideoLayout -class RTMPInjection: BaseViewController { - @IBOutlet weak var joinButton: UIButton! - @IBOutlet weak var channelTextField: UITextField! +class RTMPInjectionEntry : UIViewController +{ + @IBOutlet weak var joinButton: AGButton! + @IBOutlet weak var channelTextField: AGTextField! + let identifier = "RTMPInjection" + + override func viewDidLoad() { + super.viewDidLoad() + } + + @IBAction func doJoinPressed(sender: AGButton) { + guard let channelName = channelTextField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName":channelName] + self.navigationController?.pushViewController(newViewController, animated: true) + } +} + +class RTMPInjectionMain: BaseViewController { @IBOutlet weak var pullButton: UIButton! @IBOutlet weak var rtmpTextField: UITextField! + @IBOutlet weak var videoContainer: AGEVideoContainer! + @IBOutlet weak var rtmpContainer: AGEVideoContainer! // indicate if current instance has joined channel var isJoined: Bool = false { didSet { - channelTextField.isEnabled = !isJoined - joinButton.isHidden = isJoined rtmpTextField.isHidden = !isJoined pullButton.isHidden = !isJoined } } - var localVideo = VideoView(frame: CGRect.zero) - var remoteVideo = VideoView(frame: CGRect.zero) + var localVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + var remoteVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) var rtmpVideo = VideoView(frame: CGRect.zero) var agoraKit: AgoraRtcEngineKit! var remoteUid: UInt? @@ -35,59 +57,38 @@ class RTMPInjection: BaseViewController { override func viewDidLoad() { super.viewDidLoad() - // set up agora instance when view loaded - agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: KeyCenter.AppId, delegate: self) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - // leave channel when exiting the view - if(isJoined) { - if let rtmpURL = rtmpURL { - agoraKit.removeInjectStreamUrl(rtmpURL) - } - - agoraKit.leaveChannel { (stats) -> Void in - LogUtils.log(msg: "left channel, duration: \(stats.duration)", level: .info) - } - } - } - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - guard let identifier = segue.identifier else { - return - } - switch identifier { - case "RTCStreamRenderView": - let vc = segue.destination as! RenderViewController - vc.layoutStream(views: [localVideo, remoteVideo]) - case "RTMPStreamRenderView": - let vc = segue.destination as! RenderViewController - vc.layoutStream(views: [rtmpVideo]) - default: - break - } - } - - override func touchesBegan(_ touches: Set, with event: UIEvent?) { - view.endEditing(true) - } - - /// callback when join button hit - @IBAction func doJoinChannelPressed () { - guard let channelName = channelTextField.text else {return} + localVideo.setPlaceholder(text: "Local Host".localized) + remoteVideo.setPlaceholder(text: "Remote Host".localized) + videoContainer.layoutStream(views: [localVideo, remoteVideo]) - // resign channelTextField - channelTextField.resignFirstResponder() + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + guard let channelName = configs["channelName"] as? String, + let resolution = GlobalSettings.shared.getSetting(key: "resolution")?.selectedOption().value as? CGSize, + let fps = GlobalSettings.shared.getSetting(key: "fps")?.selectedOption().value as? AgoraVideoFrameRate, + let orientation = GlobalSettings.shared.getSetting(key: "orientation")?.selectedOption().value as? AgoraVideoOutputOrientationMode else {return} + + // make myself a broadcaster + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) // enable video module and set up video encoding configs agoraKit.enableVideo() - agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: AgoraVideoDimension320x240, - frameRate: .fps15, + agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: resolution, + frameRate: fps, bitrate: AgoraVideoBitrateStandard, - orientationMode: .adaptative)) - + orientationMode: orientation)) + // set up local video to render your local camera preview let videoCanvas = AgoraRtcVideoCanvas() videoCanvas.uid = 0 @@ -95,32 +96,49 @@ class RTMPInjection: BaseViewController { videoCanvas.view = localVideo.videoView videoCanvas.renderMode = .hidden agoraKit.setupLocalVideo(videoCanvas) - + // Set audio route to speaker agoraKit.setDefaultAudioRouteToSpeakerphone(true) - + // start joining channel // 1. Users can only see each other after they join the // same channel successfully using the same app id. // 2. If app certificate is turned on at dashboard, token is needed // when joining channel. The channel name and uid used to calculate // the token has to match the ones used for channel join - let result = agoraKit.joinChannel(byToken: nil, - channelId: channelName, - info: nil, - uid: 0) { [unowned self] (channel, uid, elapsed) -> Void in - self.isJoined = true - } - + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channelName, info: nil, uid: 0, options: option) if (result != 0) { // Usually happens with invalid parameters // Error code description can be found at: // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html - self.showAlert(title: "Error", msg: "joinChannel call failed: \(result), please check your params") + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + if isJoined { + if let rtmpURL = rtmpURL { + agoraKit.removeInjectStreamUrl(rtmpURL) + } + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } } } + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + view.endEditing(true) + } + + /// callback when join button hit + @IBAction func doJoinChannelPressed () { + } + /// callback when pull button hit @IBAction func doPullPressed () { guard let rtmpURL = rtmpTextField.text else { @@ -138,7 +156,7 @@ class RTMPInjection: BaseViewController { } /// agora rtc engine delegate events -extension RTMPInjection: AgoraRtcEngineDelegate { +extension RTMPInjectionMain: AgoraRtcEngineDelegate { /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out /// what is happening /// Warning code description can be found at: @@ -146,7 +164,7 @@ extension RTMPInjection: AgoraRtcEngineDelegate { /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html /// @param warningCode warning code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { - LogUtils.log(msg: "warning: \(warningCode.description)", level: .warning) + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) } /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand @@ -156,14 +174,24 @@ extension RTMPInjection: AgoraRtcEngineDelegate { /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html /// @param errorCode error code of the problem func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { - LogUtils.log(msg: "error: \(errorCode.description)", level: .error) + LogUtils.log(message: "error: \(errorCode.description)", level: .error) + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) } /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event /// @param uid uid of remote joined user /// @param elapsed time elapse since current sdk instance join the channel in ms func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { - LogUtils.log(msg: "remote user join: \(uid) \(elapsed)ms", level: .info) + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) /// RTMP Inject stream uid is always 666 if uid != 666 { @@ -195,7 +223,7 @@ extension RTMPInjection: AgoraRtcEngineDelegate { /// @param reason reason why this user left, note this event may be triggered when the remote user /// become an audience in live broadcasting profile func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { - LogUtils.log(msg: "remote user left: \(uid) reason \(reason.rawValue)", level: .info) + LogUtils.log(message: "remote user left: \(uid) reason \(reason.rawValue)", level: .info) // to unlink your view from sdk, so that your view reference will be released // note the video will stay at its last frame, to completely remove it @@ -214,11 +242,11 @@ extension RTMPInjection: AgoraRtcEngineDelegate { /// @param uid User ID. /// @param status Status of the externally injected stream. See AgoraInjectStreamStatus. func rtcEngine(_ engine: AgoraRtcEngineKit, streamInjectedStatusOfUrl url: String, uid: UInt, status: AgoraInjectStreamStatus) { - LogUtils.log(msg: "rtmp injection: \(url) status \(status.rawValue)", level: .info) + LogUtils.log(message: "rtmp injection: \(url) status \(status.rawValue)", level: .info) if status == .startSuccess { - self.showAlert(title: "Notice", msg: "RTMP Inject Success") + self.showAlert(title: "Notice", message: "RTMP Inject Success") } else if status == .startFailed { - self.showAlert(title: "Error", msg: "RTMP Inject Failed") + self.showAlert(title: "Error", message: "RTMP Inject Failed") } } } diff --git a/iOS/APIExample/Examples/Advanced/RTMPInjection/zh-Hans.lproj/RTMPInjection.strings b/iOS/APIExample/Examples/Advanced/RTMPInjection/zh-Hans.lproj/RTMPInjection.strings new file mode 100644 index 000000000..a43b95a45 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/RTMPInjection/zh-Hans.lproj/RTMPInjection.strings @@ -0,0 +1,12 @@ + +/* Class = "UIButton"; normalTitle = "Pull"; ObjectID = "1nQ-Pr-sMK"; */ +"1nQ-Pr-sMK.normalTitle" = "拉流"; + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UITextField"; placeholder = "Enter RTMP URL"; ObjectID = "V9k-cL-4Yp"; */ +"V9k-cL-4Yp.placeholder" = "输入媒体流地址"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; diff --git a/iOS/APIExample/Examples/Advanced/RTMPStreaming/Base.lproj/RTMPStreaming.storyboard b/iOS/APIExample/Examples/Advanced/RTMPStreaming/Base.lproj/RTMPStreaming.storyboard new file mode 100644 index 000000000..a48ee05e3 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/RTMPStreaming/Base.lproj/RTMPStreaming.storyboard @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/RTMPStreaming/RTMPStreaming.swift b/iOS/APIExample/Examples/Advanced/RTMPStreaming/RTMPStreaming.swift new file mode 100644 index 000000000..d92b636b5 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/RTMPStreaming/RTMPStreaming.swift @@ -0,0 +1,307 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import Foundation +import UIKit +import AgoraRtcKit +import AGEVideoLayout + +let CANVAS_WIDTH = 640 +let CANVAS_HEIGHT = 480 + +class RTMPStreamingEntry : UIViewController +{ + @IBOutlet weak var joinButton: AGButton! + @IBOutlet weak var channelTextField: AGTextField! + @IBOutlet weak var noteLabel: UILabel! + let identifier = "RTMPStreaming" + + override func viewDidLoad() { + super.viewDidLoad() + noteLabel.text = "Ensure that you enable the RTMP Converter service at Agora Dashboard before using this function." + } + + @IBAction func doJoinPressed(sender: AGButton) { + guard let channelName = channelTextField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName":channelName] + self.navigationController?.pushViewController(newViewController, animated: true) + } +} + +class RTMPStreamingMain: BaseViewController { + @IBOutlet weak var publishButton: UIButton! + @IBOutlet weak var rtmpTextField: UITextField! + @IBOutlet weak var transcodingSwitch: UISwitch! + @IBOutlet weak var transcodingLabel: UILabel! + @IBOutlet weak var container: AGEVideoContainer! + + // indicate if current instance has joined channel + var isJoined: Bool = false { + didSet { + rtmpTextField.isEnabled = isJoined + publishButton.isEnabled = isJoined + transcodingSwitch.isEnabled = isJoined + } + } + + var isPublished: Bool = false { + didSet { + rtmpTextField.isEnabled = !isPublished + transcodingSwitch.isEnabled = !isPublished + publishButton.title = isPublished ? "stop" : "push" + } + } + + var localVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + var remoteVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + var agoraKit: AgoraRtcEngineKit! + var remoteUid: UInt? + var rtmpURL: String? + var transcoding = AgoraLiveTranscoding.default() + + override func viewDidLoad() { + super.viewDidLoad() + // layout render view + localVideo.setPlaceholder(text: "Local Host".localized) + remoteVideo.setPlaceholder(text: "Remote Host".localized) + container.layoutStream(views: [localVideo, remoteVideo]) + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + guard let channelName = configs["channelName"] as? String, + let resolution = GlobalSettings.shared.getSetting(key: "resolution")?.selectedOption().value as? CGSize, + let fps = GlobalSettings.shared.getSetting(key: "fps")?.selectedOption().value as? AgoraVideoFrameRate, + let orientation = GlobalSettings.shared.getSetting(key: "orientation")?.selectedOption().value as? AgoraVideoOutputOrientationMode else {return} + + // make myself a broadcaster + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) + + // enable video module and set up video encoding configs + agoraKit.enableVideo() + agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: resolution, + frameRate: fps, + bitrate: AgoraVideoBitrateStandard, + orientationMode: orientation)) + + // set up local video to render your local camera preview + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + // the view to be binded + videoCanvas.view = localVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channelName, info: nil, uid: 0, options: option) + if (result != 0) { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + if isJoined { + if let rtmpURL = rtmpURL { + agoraKit.removePublishStreamUrl(rtmpURL) + } + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + } + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + view.endEditing(true) + } + + /// callback when publish button hit + @IBAction func onPublish() { + guard let rtmpURL = rtmpTextField.text else { + return + } + if(isPublished) { + // stop rtmp streaming + agoraKit.removePublishStreamUrl(rtmpURL) + } else { + // resign rtmp text field + rtmpTextField.resignFirstResponder() + + // check whether we need to enable transcoding + let transcodingEnabled = transcodingSwitch.isOn + if(transcodingEnabled){ + // we will use transcoding to composite multiple hosts' video + // therefore we have to create a livetranscoding object and call before addPublishStreamUrl + transcoding.size = CGSize(width: CANVAS_WIDTH, height: CANVAS_HEIGHT) + agoraKit.setLiveTranscoding(transcoding) + } + agoraKit.addPublishStreamUrl(rtmpURL, transcodingEnabled: transcodingEnabled) + + self.rtmpURL = rtmpURL + } + } +} + +/// agora rtc engine delegate events +extension RTMPStreamingMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode.description)", level: .error) + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + + // add transcoding user so the video stream will be involved + // in future RTMP Stream + let user = AgoraLiveTranscodingUser() + user.rect = CGRect(x: 0, y: 0, width: CANVAS_WIDTH / 2, height: CANVAS_HEIGHT) + user.uid = uid + transcoding.add(user) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // only one remote video view is available for this + // tutorial. Here we check if there exists a surface + // view tagged as this uid. + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + + // remove preivous user from the canvas + if let existingUid = remoteUid { + transcoding.removeUser(existingUid) + } + remoteUid = uid + + // check whether we have enabled transcoding + let transcodingEnabled = transcodingSwitch.isOn + if(transcodingEnabled){ + // add new user onto the canvas + let user = AgoraLiveTranscodingUser() + user.rect = CGRect(x: CANVAS_WIDTH / 2, y: 0, width: CANVAS_WIDTH / 2, height: CANVAS_HEIGHT) + user.uid = uid + self.transcoding.add(user) + // remember you need to call setLiveTranscoding again if you changed the layout + agoraKit.setLiveTranscoding(transcoding) + } + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason.rawValue)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + + // check whether we have enabled transcoding + let transcodingEnabled = transcodingSwitch.isOn + if(transcodingEnabled){ + // remove user from canvas if current cohost left channel + if let existingUid = remoteUid { + transcoding.removeUser(existingUid) + } + remoteUid = nil + // remember you need to call setLiveTranscoding again if you changed the layout + agoraKit.setLiveTranscoding(transcoding) + } + } + + /// callback for state of rtmp streaming, for both good and bad state + /// @param url rtmp streaming url + /// @param state state of rtmp streaming + /// @param reason + func rtcEngine(_ engine: AgoraRtcEngineKit, rtmpStreamingChangedToState url: String, state: AgoraRtmpStreamingState, errorCode: AgoraRtmpStreamingErrorCode) { + LogUtils.log(message: "rtmp streaming: \(url) state \(state.rawValue) error \(errorCode.rawValue)", level: .info) + if(state == .running) { + self.showAlert(title: "Notice", message: "RTMP Publish Success") + isPublished = true + } else if(state == .failure) { + self.showAlert(title: "Error", message: "RTMP Publish Failed: \(errorCode.rawValue)") + } else if(state == .idle) { + self.showAlert(title: "Notice", message: "RTMP Publish Stopped") + isPublished = false + } + } + + /// callback when live transcoding is properly updated + func rtcEngineTranscodingUpdated(_ engine: AgoraRtcEngineKit) { + LogUtils.log(message: "live transcoding updated", level: .info) + } +} diff --git a/iOS/APIExample/Examples/Advanced/RTMPStreaming/zh-Hans.lproj/RTMPStreaming.strings b/iOS/APIExample/Examples/Advanced/RTMPStreaming/zh-Hans.lproj/RTMPStreaming.strings new file mode 100644 index 000000000..edf6a7dfe --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/RTMPStreaming/zh-Hans.lproj/RTMPStreaming.strings @@ -0,0 +1,18 @@ + +/* Class = "UIButton"; normalTitle = "Publish"; ObjectID = "6UB-N4-z8k"; */ +"6UB-N4-z8k.normalTitle" = "推流"; + +/* Class = "UITextField"; placeholder = "Enter RTMP URL"; ObjectID = "8Mz-FP-egY"; */ +"8Mz-FP-egY.placeholder" = "输入RTMP推流地址"; + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UINavigationItem"; title = "RTMP Streaming"; ObjectID = "Iif-xT-wDr"; */ +"Iif-xT-wDr.title" = "RTMP旁路推流"; + +/* Class = "UILabel"; text = "Transcoding"; ObjectID = "cVh-mr-jY1"; */ +"cVh-mr-jY1.text" = "转码"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; diff --git a/iOS/APIExample/Examples/Advanced/RawAudioData/Base.lproj/RawAudioData.storyboard b/iOS/APIExample/Examples/Advanced/RawAudioData/Base.lproj/RawAudioData.storyboard new file mode 100644 index 000000000..10624c176 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/RawAudioData/Base.lproj/RawAudioData.storyboard @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/RawAudioData/RawAudioData.swift b/iOS/APIExample/Examples/Advanced/RawAudioData/RawAudioData.swift new file mode 100644 index 000000000..ca8b7e493 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/RawAudioData/RawAudioData.swift @@ -0,0 +1,231 @@ +// +// RawAudioData.swift +// APIExample +// +// Created by XC on 2020/12/30. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import UIKit +import AGEVideoLayout +import AgoraRtcKit + +class RawAudioDataEntry: UIViewController { + + @IBOutlet weak var joinButton: AGButton! + @IBOutlet weak var channelTextField: AGTextField! + + let identifier = "RawAudioData" + + @IBAction func doJoinPressed(sender: AGButton) { + guard let channelName = channelTextField.text else { return } + //resign channel text field + channelTextField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName": channelName] + self.navigationController?.pushViewController(newViewController, animated: true) + } + +} + +class RawAudioDataMain: BaseViewController { + var localVideo = Bundle.loadVideoView(type: .local, audioOnly: true) + var remoteVideo = Bundle.loadVideoView(type: .remote, audioOnly: true) + + @IBOutlet weak var container: AGEVideoContainer! + var agoraKit: AgoraRtcEngineKit! + //var agoraMediaDataPlugin: AgoraMediaDataPlugin? + var remoteUid: UInt? + + // indicate if current instance has joined channel + var isJoined: Bool = false + + override func viewDidLoad() { + super.viewDidLoad() + // layout render view + container.layoutStream(views: [localVideo, remoteVideo]) + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + // get channel name from configs + guard let channelName = configs["channelName"] as? String else { return } + // disable video module in audio scene + agoraKit.disableVideo() + // make myself a broadcaster + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) + // Register audio observer + agoraKit.setAudioFrameDelegate(self) + + agoraKit.setRecordingAudioFrameParametersWithSampleRate(44100, channel: 1, mode: .readWrite, samplesPerCall: 4410) + agoraKit.setMixedAudioFrameParametersWithSampleRate(44100, samplesPerCall: 4410) + agoraKit.setPlaybackAudioFrameParametersWithSampleRate(44100, channel: 1, mode: .readWrite, samplesPerCall: 4410) + + // set up local video to render your local camera preview + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + // the view to be binded + videoCanvas.view = localVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channelName, info: nil, uid: 0, options: option) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + if isJoined { + // deregister observers + agoraKit.leaveChannel { (stats) -> Void in + // unregister AudioFrameDelegate + self.agoraKit.setAudioFrameDelegate(nil) + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + } + } +} + +/// agora rtc engine delegate events +extension RawAudioDataMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + localVideo.setPlaceholder(text: self.getAudioLabel(uid: uid, isLocal: true)) + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // Only one remote video view is available for this + // tutorial. Here we check if there exists a surface + // view tagged as this uid. + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videoView + videoCanvas.renderMode = .hidden + remoteUid = uid + remoteVideo.setPlaceholder(text: self.getAudioLabel(uid: uid, isLocal: false)) + agoraKit.setupRemoteVideo(videoCanvas) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + remoteUid = nil + agoraKit.setupRemoteVideo(videoCanvas) + } + + /// Reports the statistics of the current call. The SDK triggers this callback once every two seconds after the user joins the channel. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, reportRtcStats stats: AgoraChannelStats) { + localVideo.statsInfo?.updateChannelStats(stats) + } + + /// Reports the statistics of the uploading local audio streams once every two seconds. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, localAudioStats stats: AgoraRtcLocalAudioStats) { + localVideo.statsInfo?.updateLocalAudioStats(stats) + } + + /// Reports the statistics of the audio stream from each remote user/host. + /// @param stats stats struct for current call statistics + func rtcEngine(_ engine: AgoraRtcEngineKit, remoteAudioStats stats: AgoraRtcRemoteAudioStats) { + if stats.uid == remoteUid { + remoteVideo.statsInfo?.updateAudioStats(stats) + } + } +} + +// audio data plugin, here you can process raw audio data +// note this all happens in CPU so it comes with a performance cost +extension RawAudioDataMain: AgoraAudioFrameDelegate { + func onRecord(_ frame: AgoraAudioFrame) -> Bool { + return true + } + + func onPlaybackAudioFrame(_ frame: AgoraAudioFrame) -> Bool { + return true + } + + func onMixedAudioFrame(_ frame: AgoraAudioFrame) -> Bool { + return true + } + + func onPlaybackAudioFrame(beforeMixing frame: AgoraAudioFrame, uid: UInt) -> Bool { + return true + } +} diff --git a/iOS/APIExample/Examples/Advanced/RawAudioData/zh-Hans.lproj/RawAudioData.strings b/iOS/APIExample/Examples/Advanced/RawAudioData/zh-Hans.lproj/RawAudioData.strings new file mode 100644 index 000000000..8641435b0 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/RawAudioData/zh-Hans.lproj/RawAudioData.strings @@ -0,0 +1,6 @@ + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "Jt2-44-4kZ"; */ +"Jt2-44-4kZ.placeholder" = "输入频道名"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "g12-XK-fOL"; */ +"g12-XK-fOL.normalTitle" = "加入频道"; diff --git a/iOS/APIExample/Examples/Advanced/RawMediaData/Base.lproj/RawMediaData.storyboard b/iOS/APIExample/Examples/Advanced/RawMediaData/Base.lproj/RawMediaData.storyboard new file mode 100644 index 000000000..99e40e782 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/RawMediaData/Base.lproj/RawMediaData.storyboard @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/RawMediaData/RawMediaData.swift b/iOS/APIExample/Examples/Advanced/RawMediaData/RawMediaData.swift new file mode 100644 index 000000000..5a15c8b02 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/RawMediaData/RawMediaData.swift @@ -0,0 +1,305 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import UIKit +import AGEVideoLayout +import AgoraRtcKit + +class RawMediaDataEntry : UIViewController +{ + @IBOutlet weak var joinButton: AGButton! + @IBOutlet weak var channelTextField: AGTextField! + let identifier = "RawMediaData" + + override func viewDidLoad() { + super.viewDidLoad() + } + + @IBAction func doJoinPressed(sender: AGButton) { + guard let channelName = channelTextField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName":channelName] + self.navigationController?.pushViewController(newViewController, animated: true) + } +} + +class RawMediaDataMain: BaseViewController { + var localVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + var remoteVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + + @IBOutlet weak var container: AGEVideoContainer! + var agoraKit: AgoraRtcEngineKit! + var agoraMediaDataPlugin: AgoraMediaDataPlugin? + var remoteUid: UInt? + + // indicate if current instance has joined channel + var isJoined: Bool = false + + override func viewDidLoad() { + super.viewDidLoad() + // layout render view + localVideo.setPlaceholder(text: "Local Host".localized) + remoteVideo.setPlaceholder(text: "Remote Host".localized) + container.layoutStream(views: [localVideo, remoteVideo]) + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + // get channel name from configs + guard let channelName = configs["channelName"] as? String, + let resolution = GlobalSettings.shared.getSetting(key: "resolution")?.selectedOption().value as? CGSize, + let fps = GlobalSettings.shared.getSetting(key: "fps")?.selectedOption().value as? AgoraVideoFrameRate, + let orientation = GlobalSettings.shared.getSetting(key: "orientation")?.selectedOption().value as? AgoraVideoOutputOrientationMode else {return} + + // make myself a broadcaster + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) + + + // enable video module and set up video encoding configs + agoraKit.enableVideo() + agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: resolution, + frameRate: fps, + bitrate: AgoraVideoBitrateStandard, + orientationMode: orientation)) + + // setup raw media data observers + agoraMediaDataPlugin = AgoraMediaDataPlugin(agoraKit: agoraKit) + + // Register audio observer + let audioType:ObserverAudioType = ObserverAudioType(rawValue: ObserverAudioType.recordAudio.rawValue | ObserverAudioType.playbackAudioFrameBeforeMixing.rawValue | ObserverAudioType.mixedAudio.rawValue | ObserverAudioType.playbackAudio.rawValue); + agoraMediaDataPlugin?.registerAudioRawDataObserver(audioType) + agoraMediaDataPlugin?.audioDelegate = self + + agoraKit.setRecordingAudioFrameParametersWithSampleRate(44100, channel: 1, mode: .readWrite, samplesPerCall: 4410) + agoraKit.setMixedAudioFrameParametersWithSampleRate(44100, samplesPerCall: 4410) + agoraKit.setPlaybackAudioFrameParametersWithSampleRate(44100, channel: 1, mode: .readWrite, samplesPerCall: 4410) + + // Register video observer + let videoType:ObserverVideoType = ObserverVideoType(rawValue: ObserverVideoType.captureVideo.rawValue | ObserverVideoType.renderVideo.rawValue | ObserverVideoType.preEncodeVideo.rawValue) + agoraMediaDataPlugin?.registerVideoRawDataObserver(videoType) + agoraMediaDataPlugin?.videoDelegate = self; + + // Register packet observer + let packetType:ObserverPacketType = ObserverPacketType(rawValue: ObserverPacketType.sendAudio.rawValue | ObserverPacketType.sendVideo.rawValue | ObserverPacketType.receiveAudio.rawValue | ObserverPacketType.receiveVideo.rawValue) + agoraMediaDataPlugin?.registerPacketRawDataObserver(packetType) + agoraMediaDataPlugin?.packetDelegate = self; + + + + // set up local video to render your local camera preview + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + // the view to be binded + videoCanvas.view = localVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channelName, info: nil, uid: 0, options: option) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + @IBAction func onSnapshot(_btn: UIButton) { + guard let uid = remoteUid else {return} + agoraMediaDataPlugin?.remoteSnapshot(withUid: uid, image: { (image:UIImage) in + UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) + }) + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + if isJoined { + // deregister observers + agoraMediaDataPlugin?.deregisterAudioRawDataObserver(ObserverAudioType(rawValue: 0)) + agoraMediaDataPlugin?.deregisterVideoRawDataObserver(ObserverVideoType(rawValue: 0)) + agoraMediaDataPlugin?.deregisterPacketRawDataObserver(ObserverPacketType(rawValue: 0)) + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + } + } +} + +/// agora rtc engine delegate events +extension RawMediaDataMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // Only one remote video view is available for this + // tutorial. Here we check if there exists a surface + // view tagged as this uid. + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videoView + videoCanvas.renderMode = .hidden + remoteUid = uid + agoraKit.setupRemoteVideo(videoCanvas) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + remoteUid = nil + agoraKit.setupRemoteVideo(videoCanvas) + } +} + +// audio data plugin, here you can process raw audio data +// note this all happens in CPU so it comes with a performance cost +extension RawMediaDataMain : AgoraAudioDataPluginDelegate +{ + /// Retrieves the recorded audio frame. + func mediaDataPlugin(_ mediaDataPlugin: AgoraMediaDataPlugin, didRecord audioRawData: AgoraAudioRawData) -> AgoraAudioRawData { + return audioRawData + } + + /// Retrieves the audio playback frame for getting the audio. + func mediaDataPlugin(_ mediaDataPlugin: AgoraMediaDataPlugin, willPlaybackAudioRawData audioRawData: AgoraAudioRawData) -> AgoraAudioRawData { + return audioRawData + } + + /// Retrieves the audio frame of a specified user before mixing. + /// The SDK triggers this callback if isMultipleChannelFrameWanted returns false. + func mediaDataPlugin(_ mediaDataPlugin: AgoraMediaDataPlugin, willPlaybackBeforeMixing audioRawData: AgoraAudioRawData, ofUid uid: uint) -> AgoraAudioRawData { + return audioRawData + } + + /// Retrieves the mixed recorded and playback audio frame. + func mediaDataPlugin(_ mediaDataPlugin: AgoraMediaDataPlugin, didMixedAudioRawData audioRawData: AgoraAudioRawData) -> AgoraAudioRawData { + return audioRawData + } +} + +// video data plugin, here you can process raw video data +// note this all happens in CPU so it comes with a performance cost +extension RawMediaDataMain : AgoraVideoDataPluginDelegate +{ + /// Occurs each time the SDK receives a video frame captured by the local camera. + /// After you successfully register the video frame observer, the SDK triggers this callback each time a video frame is received. In this callback, you can get the video data captured by the local camera. You can then pre-process the data according to your scenarios. + /// After pre-processing, you can send the processed video data back to the SDK by setting the videoFrame parameter in this callback. + func mediaDataPlugin(_ mediaDataPlugin: AgoraMediaDataPlugin, didCapturedVideoRawData videoRawData: AgoraVideoRawData) -> AgoraVideoRawData { + return videoRawData + } + + /// Occurs each time the SDK receives a video frame before sending to encoder + /// After you successfully register the video frame observer, the SDK triggers this callback each time a video frame is going to be sent to encoder. In this callback, you can get the video data before it is sent to enoder. You can then pre-process the data according to your scenarios. + /// After pre-processing, you can send the processed video data back to the SDK by setting the videoFrame parameter in this callback. + func mediaDataPlugin(_ mediaDataPlugin: AgoraMediaDataPlugin, willPreEncode videoRawData: AgoraVideoRawData) -> AgoraVideoRawData { + return videoRawData + } + + /// Occurs each time the SDK receives a video frame sent by the remote user. + ///After you successfully register the video frame observer and isMultipleChannelFrameWanted return false, the SDK triggers this callback each time a video frame is received. In this callback, you can get the video data sent by the remote user. You can then post-process the data according to your scenarios. + ///After post-processing, you can send the processed data back to the SDK by setting the videoFrame parameter in this callback. + func mediaDataPlugin(_ mediaDataPlugin: AgoraMediaDataPlugin, willRenderVideoRawData videoRawData: AgoraVideoRawData, ofUid uid: uint) -> AgoraVideoRawData { + return videoRawData + } +} + +// packet data plugin, here you can process raw network packet(before decoding/encoding) +// note this all happens in CPU so it comes with a performance cost +extension RawMediaDataMain : AgoraPacketDataPluginDelegate +{ + /// Occurs when the local user sends a video packet. + func mediaDataPlugin(_ mediaDataPlugin: AgoraMediaDataPlugin, willSendVideoPacket videoPacket: AgoraPacketRawData) -> AgoraPacketRawData { + return videoPacket + } + + /// Occurs when the local user sends an audio packet. + func mediaDataPlugin(_ mediaDataPlugin: AgoraMediaDataPlugin, willSendAudioPacket audioPacket: AgoraPacketRawData) -> AgoraPacketRawData { + return audioPacket + } + + /// Occurs when the local user receives a video packet. + func mediaDataPlugin(_ mediaDataPlugin: AgoraMediaDataPlugin, didReceivedVideoPacket videoPacket: AgoraPacketRawData) -> AgoraPacketRawData { + return videoPacket + } + + /// Occurs when the local user receives an audio packet. + func mediaDataPlugin(_ mediaDataPlugin: AgoraMediaDataPlugin, didReceivedAudioPacket audioPacket: AgoraPacketRawData) -> AgoraPacketRawData { + return audioPacket + } +} diff --git a/iOS/APIExample/Examples/Advanced/RawMediaData/zh-Hans.lproj/RawMediaData.strings b/iOS/APIExample/Examples/Advanced/RawMediaData/zh-Hans.lproj/RawMediaData.strings new file mode 100644 index 000000000..81f679e41 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/RawMediaData/zh-Hans.lproj/RawMediaData.strings @@ -0,0 +1,12 @@ + +/* Class = "UINavigationItem"; title = "Join Channel"; ObjectID = "AmK-zc-ByT"; */ +"AmK-zc-ByT.title" = "加入频道"; + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UIViewController"; title = "Join Channel Video"; ObjectID = "cAG-6V-STC"; */ +"cAG-6V-STC.title" = "音视频裸数据"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; diff --git a/iOS/APIExample/Examples/Advanced/ScreenShare/Base.lproj/ScreenShare.storyboard b/iOS/APIExample/Examples/Advanced/ScreenShare/Base.lproj/ScreenShare.storyboard new file mode 100644 index 000000000..72042f519 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/ScreenShare/Base.lproj/ScreenShare.storyboard @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/ScreenShare/ScreenShare.swift b/iOS/APIExample/Examples/Advanced/ScreenShare/ScreenShare.swift new file mode 100644 index 000000000..5cd66cc3a --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/ScreenShare/ScreenShare.swift @@ -0,0 +1,223 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import UIKit +import AGEVideoLayout +import AgoraRtcKit +import ReplayKit + +class ScreenShareEntry : UIViewController +{ + @IBOutlet weak var joinButton: UIButton! + @IBOutlet weak var channelTextField: UITextField! + let identifier = "ScreenShare" + + override func viewDidLoad() { + super.viewDidLoad() + } + + @IBAction func doJoinPressed(sender: UIButton) { + guard let channelName = channelTextField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName":channelName] + self.navigationController?.pushViewController(newViewController, animated: true) + } +} + +class ScreenShareMain: BaseViewController { + var localVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + var remoteVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + + @IBOutlet weak var container: AGEVideoContainer! + @IBOutlet weak var broadcasterPickerContainer: UIView! + var agoraKit: AgoraRtcEngineKit! + + // indicate if current instance has joined channel + var isJoined: Bool = false + + override func viewDidLoad() { + super.viewDidLoad() + + // prepare system broadcaster picker + prepareSystemBroadcaster() + + // layout render view + localVideo.setPlaceholder(text: "Local Host".localized) + remoteVideo.setPlaceholder(text: "Remote Host".localized) + container.layoutStream(views: [localVideo, remoteVideo]) + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + // get channel name from configs + guard let channelName = configs["channelName"] as? String, + let resolution = GlobalSettings.shared.getSetting(key: "resolution")?.selectedOption().value as? CGSize, + let fps = GlobalSettings.shared.getSetting(key: "fps")?.selectedOption().value as? AgoraVideoFrameRate, + let orientation = GlobalSettings.shared.getSetting(key: "orientation")?.selectedOption().value as? AgoraVideoOutputOrientationMode else {return} + + + // make myself a broadcaster + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) + + // enable video module and set up video encoding configs + agoraKit.enableVideo() + agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: resolution, + frameRate: fps, + bitrate: AgoraVideoBitrateStandard, + orientationMode: orientation)) + + // set up local video to render your local camera preview + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + // the view to be binded + videoCanvas.view = localVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channelName, info: nil, uid: 0, options: option) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + func prepareSystemBroadcaster() { + if #available(iOS 12.0, *) { + let frame = CGRect(x: 0, y:0, width: 60, height: 60) + let systemBroadcastPicker = RPSystemBroadcastPickerView(frame: frame) + systemBroadcastPicker.autoresizingMask = [.flexibleTopMargin, .flexibleRightMargin] + if let url = Bundle.main.url(forResource: "Agora-ScreenShare-Extension", withExtension: "appex", subdirectory: "PlugIns") { + if let bundle = Bundle(url: url) { + systemBroadcastPicker.preferredExtension = bundle.bundleIdentifier + } + } + broadcasterPickerContainer.addSubview(systemBroadcastPicker) + } else { + self.showAlert(message: "Minimum support iOS version is 12.0") + } + + } + + func isScreenShareUid(uid: UInt) -> Bool { + return uid >= SCREEN_SHARE_UID_MIN && uid <= SCREEN_SHARE_UID_MAX + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + // deregister packet processing + AgoraCustomEncryption.deregisterPacketProcessing(agoraKit) + if isJoined { + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + } + } +} + +/// agora rtc engine delegate events +extension ScreenShareMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + if(isScreenShareUid(uid: uid)) { + LogUtils.log(message: "Ignore screen share uid", level: .info) + return + } + + // Only one remote video view is available for this + // tutorial. Here we check if there exists a surface + // view tagged as this uid. + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + } +} diff --git a/iOS/APIExample/Examples/Advanced/ScreenShare/zh-Hans.lproj/ScreenShare.strings b/iOS/APIExample/Examples/Advanced/ScreenShare/zh-Hans.lproj/ScreenShare.strings new file mode 100644 index 000000000..29f03308c --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/ScreenShare/zh-Hans.lproj/ScreenShare.strings @@ -0,0 +1,12 @@ + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UITextField"; text = "ScreenShare"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.text" = "ScreenShare"; + +/* Class = "UIViewController"; title = "Join Channel Audio"; ObjectID = "jxp-ZN-2yG"; */ +"jxp-ZN-2yG.title" = "Join Channel Audio"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; diff --git a/iOS/APIExample/Examples/Advanced/StreamEncryption/Base.lproj/StreamEncryption.storyboard b/iOS/APIExample/Examples/Advanced/StreamEncryption/Base.lproj/StreamEncryption.storyboard new file mode 100644 index 000000000..5f3c11c6e --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/StreamEncryption/Base.lproj/StreamEncryption.storyboard @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/StreamEncryption/StreamEncryption.swift b/iOS/APIExample/Examples/Advanced/StreamEncryption/StreamEncryption.swift new file mode 100644 index 000000000..6ce580f71 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/StreamEncryption/StreamEncryption.swift @@ -0,0 +1,241 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import UIKit +import AGEVideoLayout +import AgoraRtcKit + +class StreamEncryptionEntry : UIViewController +{ + @IBOutlet weak var joinButton: UIButton! + @IBOutlet weak var channelTextField: UITextField! + @IBOutlet weak var encryptSecretField: UITextField! + @IBOutlet weak var encryptModeBtn: UIButton! + var mode:AgoraEncryptionMode = .AES128XTS + var useCustom:Bool = false + let identifier = "StreamEncryption" + + override func viewDidLoad() { + super.viewDidLoad() + + encryptModeBtn.setTitle("\(mode.description())", for: .normal) + } + + @IBAction func doJoinPressed(sender: UIButton) { + guard let channelName = channelTextField.text, let secret = encryptSecretField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() + encryptSecretField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName":channelName, "mode":mode, "secret":secret, "useCustom": useCustom] + self.navigationController?.pushViewController(newViewController, animated: true) + } + + func getEncryptionModeAction(_ mode:AgoraEncryptionMode) -> UIAlertAction{ + return UIAlertAction(title: "\(mode.description())", style: .default, handler: {[unowned self] action in + self.mode = mode + self.useCustom = false + self.encryptModeBtn.setTitle("\(mode.description())", for: .normal) + }) + } + + @IBAction func setEncryptionMode(){ + let alert = UIAlertController(title: "Set Encryption Mode".localized, message: nil, preferredStyle: .actionSheet) + for profile in AgoraEncryptionMode.allValues(){ + alert.addAction(getEncryptionModeAction(profile)) + } + // add custom option + alert.addAction(UIAlertAction(title: "Custom", style: .default, handler: { (action:UIAlertAction) in + self.useCustom = true + self.encryptModeBtn.setTitle("Custom", for: .normal) + })) + alert.addCancelAction() + present(alert, animated: true, completion: nil) + } + +} + +class StreamEncryptionMain: BaseViewController { + var localVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + var remoteVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + + @IBOutlet weak var container: AGEVideoContainer! + var agoraKit: AgoraRtcEngineKit! + + // indicate if current instance has joined channel + var isJoined: Bool = false + + override func viewDidLoad() { + super.viewDidLoad() + // layout render view + localVideo.setPlaceholder(text: "Local Host".localized) + remoteVideo.setPlaceholder(text: "Remote Host".localized) + container.layoutStream(views: [localVideo, remoteVideo]) + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + // get channel name from configs + guard let channelName = configs["channelName"] as? String, + let secret = configs["secret"] as? String, + let mode = configs["mode"] as? AgoraEncryptionMode, + let useCustom = configs["useCustom"] as? Bool, + let resolution = GlobalSettings.shared.getSetting(key: "resolution")?.selectedOption().value as? CGSize, + let fps = GlobalSettings.shared.getSetting(key: "fps")?.selectedOption().value as? AgoraVideoFrameRate, + let orientation = GlobalSettings.shared.getSetting(key: "orientation")?.selectedOption().value as? AgoraVideoOutputOrientationMode else {return} + + // make myself a broadcaster + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) + + // enable encryption + if(!useCustom) { + // sdk encryption + let config = AgoraEncryptionConfig() + config.encryptionMode = mode + config.encryptionKey = secret + let ret = agoraKit.enableEncryption(true, encryptionConfig: config) + if ret != 0 { + // for errors please take a look at: + // CN https://docs.agora.io/cn/Video/API%20Reference/oc/Classes/AgoraRtcEngineKit.html#//api/name/enableEncryption:encryptionConfig: + // EN https://docs.agora.io/en/Video/API%20Reference/oc/Classes/AgoraRtcEngineKit.html#//api/name/enableEncryption:encryptionConfig: + self.showAlert(title: "Error", message: "enableEncryption call failed: \(ret), please check your params") + } + } else { + // your own custom algorithm encryption + AgoraCustomEncryption.registerPacketProcessing(agoraKit) + } + + // enable video module and set up video encoding configs + agoraKit.enableVideo() + agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: resolution, + frameRate: fps, + bitrate: AgoraVideoBitrateStandard, + orientationMode: orientation)) + + // set up local video to render your local camera preview + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + // the view to be binded + videoCanvas.view = localVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channelName, info: nil, uid: 0, options: option) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + // deregister packet processing + AgoraCustomEncryption.deregisterPacketProcessing(agoraKit) + if isJoined { + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + } + } +} + +/// agora rtc engine delegate events +extension StreamEncryptionMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // Only one remote video view is available for this + // tutorial. Here we check if there exists a surface + // view tagged as this uid. + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + } +} diff --git a/iOS/APIExample/Examples/Advanced/StreamEncryption/zh-Hans.lproj/StreamEncryption.strings b/iOS/APIExample/Examples/Advanced/StreamEncryption/zh-Hans.lproj/StreamEncryption.strings new file mode 100644 index 000000000..9800c3222 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/StreamEncryption/zh-Hans.lproj/StreamEncryption.strings @@ -0,0 +1,18 @@ + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UILabel"; text = "Encryption Mode"; ObjectID = "Q0E-5B-IED"; */ +"Q0E-5B-IED.text" = "加密方式"; + +/* Class = "UITextField"; placeholder = "Enter encryption secret"; ObjectID = "SwF-zc-EP4"; */ +"SwF-zc-EP4.placeholder" = "加密密码"; + +/* Class = "UIViewController"; title = "Join Channel Audio"; ObjectID = "jxp-ZN-2yG"; */ +"jxp-ZN-2yG.title" = "Join Channel Audio"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "myR-6e-1zj"; */ +"myR-6e-1zj.normalTitle" = "Button"; diff --git a/iOS/APIExample/Examples/Advanced/SuperResolution/Base.lproj/SuperResolution.storyboard b/iOS/APIExample/Examples/Advanced/SuperResolution/Base.lproj/SuperResolution.storyboard new file mode 100644 index 000000000..2c594298c --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/SuperResolution/Base.lproj/SuperResolution.storyboard @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/SuperResolution/SuperResolution.swift b/iOS/APIExample/Examples/Advanced/SuperResolution/SuperResolution.swift new file mode 100644 index 000000000..61efd894a --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/SuperResolution/SuperResolution.swift @@ -0,0 +1,225 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import UIKit +import AGEVideoLayout +import AgoraRtcKit + +class SuperResolutionEntry : UIViewController +{ + @IBOutlet weak var joinButton: UIButton! + @IBOutlet weak var channelTextField: UITextField! + let identifier = "SuperResolution" + + override func viewDidLoad() { + super.viewDidLoad() + } + + @IBAction func doJoinPressed(sender: UIButton) { + guard let channelName = channelTextField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName":channelName] + self.navigationController?.pushViewController(newViewController, animated: true) + } +} + +class SuperResolutionMain: BaseViewController { + var localVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + var remoteVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + @IBOutlet weak var localVideoContainer:UIView! + @IBOutlet weak var remoteVideoContainer:UIView! + @IBOutlet weak var superResolutionToggle:UISwitch! + var agoraKit: AgoraRtcEngineKit! + var remoteUid: UInt? + + // indicate if current instance has joined channel + var isJoined: Bool = false + + override func viewDidLoad() { + super.viewDidLoad() + + // layout render view + localVideoContainer.addSubview(localVideo) + remoteVideoContainer.addSubview(remoteVideo) + localVideo.setPlaceholder(text: "Local Host".localized) + localVideo.bindFrameToSuperviewBounds() + remoteVideo.setPlaceholder(text: "Remote Host".localized) + remoteVideo.bindFrameToSuperviewBounds() + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + // get channel name from configs + guard let channelName = configs["channelName"] as? String, + let resolution = GlobalSettings.shared.getSetting(key: "resolution")?.selectedOption().value as? CGSize, + let fps = GlobalSettings.shared.getSetting(key: "fps")?.selectedOption().value as? AgoraVideoFrameRate, + let orientation = GlobalSettings.shared.getSetting(key: "orientation")?.selectedOption().value as? AgoraVideoOutputOrientationMode else {return} + + + // make myself a broadcaster + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) + + // enable video module and set up video encoding configs + agoraKit.enableVideo() + agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: resolution, + frameRate: fps, + bitrate: AgoraVideoBitrateStandard, + orientationMode: orientation)) + + // set up local video to render your local camera preview + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + // the view to be binded + videoCanvas.view = localVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: nil, channelId: channelName, info: nil, uid: SCREEN_SHARE_BROADCASTER_UID, options: option) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + @IBAction func onToggleSuperResolution(_ sender:UISwitch) { + updateSuperResolution(sender.isOn) + } + + fileprivate func updateSuperResolution(_ enabled:Bool) { + guard let uid = remoteUid else {return} + agoraKit.enableRemoteSuperResolution(uid, enabled: enabled) + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + // deregister packet processing + AgoraCustomEncryption.deregisterPacketProcessing(agoraKit) + if isJoined { + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + } + } +} + +/// agora rtc engine delegate events +extension SuperResolutionMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // Only one remote video view is available for this + // tutorial. Here we check if there exists a surface + // view tagged as this uid. + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + + // turn off super resolution if remote user exists + updateSuperResolution(false) + // record/replace remote uid + remoteUid = uid + // update super resolution if needed + updateSuperResolution(superResolutionToggle.isOn) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + + // update super resolution if needed + if(remoteUid == uid) { + updateSuperResolution(false) + remoteUid = nil + } + } + + /// callback when super resolution is enabled for a specific uid, detail reason will be provided when super resolution fail to apply + /// @param uid uid of resolution applied + /// @param on or off + /// @param reason/state of super res + func rtcEngine(_ engine: AgoraRtcEngineKit, superResolutionEnabledOfUid uid: UInt, enabled: Bool, reason: AgoraSuperResolutionStateReason) { + LogUtils.log(message: "superResolutionEnabledOfUid \(uid) \(enabled) \(reason.rawValue)", level: .info) + if(reason != .srStateReasonSuccess) { + self.showAlert(message: "super resolution enable failed: \(reason.rawValue)") + } + } +} diff --git a/iOS/APIExample/Examples/Advanced/SuperResolution/zh-Hans.lproj/SuperResolution.strings b/iOS/APIExample/Examples/Advanced/SuperResolution/zh-Hans.lproj/SuperResolution.strings new file mode 100644 index 000000000..29f03308c --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/SuperResolution/zh-Hans.lproj/SuperResolution.strings @@ -0,0 +1,12 @@ + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UITextField"; text = "ScreenShare"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.text" = "ScreenShare"; + +/* Class = "UIViewController"; title = "Join Channel Audio"; ObjectID = "jxp-ZN-2yG"; */ +"jxp-ZN-2yG.title" = "Join Channel Audio"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; diff --git a/iOS/APIExample/Examples/Advanced/VideoChat/Base.lproj/VideoChat.storyboard b/iOS/APIExample/Examples/Advanced/VideoChat/Base.lproj/VideoChat.storyboard new file mode 100644 index 000000000..8ec3f8e8b --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/VideoChat/Base.lproj/VideoChat.storyboard @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/VideoChat/VideoChat.swift b/iOS/APIExample/Examples/Advanced/VideoChat/VideoChat.swift new file mode 100644 index 000000000..d2f803e09 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/VideoChat/VideoChat.swift @@ -0,0 +1,304 @@ +// +// VideoChat.swift +// APIExample +// +// Created by XC on 2021/1/12. +// Copyright © 2021 Agora Corp. All rights reserved. +// + +import UIKit +import AgoraRtcKit +import AGEVideoLayout + +class VideoChatEntry: UIViewController { + @IBOutlet weak var joinButton: UIButton! + @IBOutlet weak var channelTextField: UITextField! + let identifier = "VideoChat" + @IBOutlet var resolutionBtn: UIButton! + @IBOutlet var fpsBtn: UIButton! + @IBOutlet var orientationBtn: UIButton! + var width:Int = 640, height:Int = 360, orientation:AgoraVideoOutputOrientationMode = .adaptative, fps: AgoraVideoFrameRate = .fps30 + + override func viewDidLoad() { + super.viewDidLoad() + resolutionBtn.setTitle("\(width)x\(height)", for: .normal) + fpsBtn.setTitle("\(fps.rawValue)fps", for: .normal) + orientationBtn.setTitle("\(orientation.description())", for: .normal) + } + + + func getResolutionAction(width:Int, height:Int) -> UIAlertAction{ + return UIAlertAction(title: "\(width)x\(height)", style: .default, handler: {[unowned self] action in + self.width = width + self.height = height + self.resolutionBtn.setTitle("\(width)x\(height)", for: .normal) + }) + } + + func getFpsAction(_ fps:AgoraVideoFrameRate) -> UIAlertAction{ + return UIAlertAction(title: "\(fps.rawValue)fps", style: .default, handler: {[unowned self] action in + self.fps = fps + self.fpsBtn.setTitle("\(fps.rawValue)fps", for: .normal) + }) + } + + func getOrientationAction(_ orientation:AgoraVideoOutputOrientationMode) -> UIAlertAction{ + return UIAlertAction(title: "\(orientation.description())", style: .default, handler: {[unowned self] action in + self.orientation = orientation + self.orientationBtn.setTitle("\(orientation.description())", for: .normal) + }) + } + + @IBAction func setResolution() { + let alert = UIAlertController(title: "Set Resolution".localized, message: nil, preferredStyle: .actionSheet) + alert.addAction(getResolutionAction(width: 90, height: 90)) + alert.addAction(getResolutionAction(width: 160, height: 120)) + alert.addAction(getResolutionAction(width: 320, height: 240)) + alert.addAction(getResolutionAction(width: 640, height: 360)) + alert.addAction(getResolutionAction(width: 1280, height: 720)) + alert.addCancelAction() + present(alert, animated: true, completion: nil) + } + + @IBAction func setFps() { + let alert = UIAlertController(title: "Set Fps".localized, message: nil, preferredStyle: .actionSheet) + alert.addAction(getFpsAction(.fps10)) + alert.addAction(getFpsAction(.fps15)) + alert.addAction(getFpsAction(.fps24)) + alert.addAction(getFpsAction(.fps30)) + alert.addAction(getFpsAction(.fps60)) + alert.addCancelAction() + present(alert, animated: true, completion: nil) + } + + @IBAction func setOrientation() { + let alert = UIAlertController(title: "Set Orientation".localized, message: nil, preferredStyle: .actionSheet) + alert.addAction(getOrientationAction(.adaptative)) + alert.addAction(getOrientationAction(.fixedLandscape)) + alert.addAction(getOrientationAction(.fixedPortrait)) + alert.addCancelAction() + present(alert, animated: true, completion: nil) + } + + @IBAction func doJoinPressed(sender: UIButton) { + guard let channelName = channelTextField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else { return } + newViewController.title = channelName + newViewController.configs = ["channelName": channelName, "resolution": CGSize(width: width, height: height), "fps": fps, "orientation": orientation] + self.navigationController?.pushViewController(newViewController, animated: true) + } +} + +class VideoChatMain: BaseViewController { + var agoraKit: AgoraRtcEngineKit! + @IBOutlet weak var container: AGEVideoContainer! + var videoViews: [UInt:VideoView] = [:] + + // indicate if current instance has joined channel + var isJoined: Bool = false + + override func viewDidLoad(){ + super.viewDidLoad() + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + // get channel name from configs + guard let channelName = configs["channelName"] as? String, + let resolution = configs["resolution"] as? CGSize, + let fps = configs["fps"] as? AgoraVideoFrameRate, + let orientation = configs["orientation"] as? AgoraVideoOutputOrientationMode else { return } + + // make myself a broadcaster + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) + + // enable video module + agoraKit.enableVideo() + agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution, + frameRate: fps, + bitrate: AgoraVideoBitrateStandard, + orientationMode: orientation + ) + ) + + // set up local video to render your local camera preview + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + + let localVideo = Bundle.loadVideoView(type: .local, audioOnly: false) + // the view to be binded + videoCanvas.view = localVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + + videoViews[0] = localVideo + container.layoutStream2x2(views: self.sortedViews()) + + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channelName, info: nil, uid: 0, options: option) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + if isJoined { + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + } + } + + func sortedViews() -> [VideoView] { + return Array(videoViews.values).sorted(by: { $0.uid < $1.uid }) + } +} + +/// agora rtc engine delegate events +extension VideoChatMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + //videoViews[0]?.uid = uid + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + let remoteVideo = Bundle.loadVideoView(type: .remote, audioOnly: false) + remoteVideo.uid = uid + + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + + self.videoViews[uid] = remoteVideo + self.container.layoutStream2x2(views: sortedViews()) + self.container.reload(level: 0, animated: true) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + + //remove remote audio view + self.videoViews.removeValue(forKey: uid) + self.container.layoutStream2x2(views: sortedViews()) + self.container.reload(level: 0, animated: true) + } + + /// Reports which users are speaking, the speakers' volumes, and whether the local user is speaking. + /// @params speakers volume info for all speakers + /// @params totalVolume Total volume after audio mixing. The value range is [0,255]. + func rtcEngine(_ engine: AgoraRtcEngineKit, reportAudioVolumeIndicationOfSpeakers speakers: [AgoraRtcAudioVolumeInfo], totalVolume: Int) { + for volumeInfo in speakers { + if let videoView = videoViews[volumeInfo.uid] { + videoView.setInfo(text: "Volume:\(volumeInfo.volume)") + } + } + } + + /// Reports the statistics of the current call. The SDK triggers this callback once every two seconds after the user joins the channel. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, reportRtcStats stats: AgoraChannelStats) { + videoViews[0]?.statsInfo?.updateChannelStats(stats) + } + + /// Reports the statistics of the uploading local video streams once every two seconds. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, localVideoStats stats: AgoraRtcLocalVideoStats) { + videoViews[0]?.statsInfo?.updateLocalVideoStats(stats) + } + + /// Reports the statistics of the uploading local audio streams once every two seconds. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, localAudioStats stats: AgoraRtcLocalAudioStats) { + videoViews[0]?.statsInfo?.updateLocalAudioStats(stats) + } + + /// Reports the statistics of the video stream from each remote user/host. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, remoteVideoStats stats: AgoraRtcRemoteVideoStats) { + videoViews[stats.uid]?.statsInfo?.updateVideoStats(stats) + } + + /// Reports the statistics of the audio stream from each remote user/host. + /// @param stats stats struct for current call statistics + func rtcEngine(_ engine: AgoraRtcEngineKit, remoteAudioStats stats: AgoraRtcRemoteAudioStats) { + videoViews[stats.uid]?.statsInfo?.updateAudioStats(stats) + } +} diff --git a/iOS/APIExample/Examples/Advanced/VideoChat/zh-Hans.lproj/VideoChat.strings b/iOS/APIExample/Examples/Advanced/VideoChat/zh-Hans.lproj/VideoChat.strings new file mode 100644 index 000000000..5fe31ab9f --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/VideoChat/zh-Hans.lproj/VideoChat.strings @@ -0,0 +1,15 @@ + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "1Oh-Mp-Kaf"; */ +"1Oh-Mp-Kaf.normalTitle" = "Button"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "6kn-IP-XVC"; */ +"6kn-IP-XVC.normalTitle" = "Button"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "bIy-JM-gZs"; */ +"bIy-JM-gZs.normalTitle" = "加入频道"; + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "jvZ-Dw-d3l"; */ +"jvZ-Dw-d3l.placeholder" = "输入频道名"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "zSZ-ED-Vyq"; */ +"zSZ-ED-Vyq.normalTitle" = "Button"; diff --git a/iOS/APIExample/Examples/Advanced/VideoMetadata/Base.lproj/VideoMetadata.storyboard b/iOS/APIExample/Examples/Advanced/VideoMetadata/Base.lproj/VideoMetadata.storyboard new file mode 100644 index 000000000..da2230de7 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/VideoMetadata/Base.lproj/VideoMetadata.storyboard @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/VideoMetadata/VideoMetadata.swift b/iOS/APIExample/Examples/Advanced/VideoMetadata/VideoMetadata.swift new file mode 100644 index 000000000..72ea2879c --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/VideoMetadata/VideoMetadata.swift @@ -0,0 +1,257 @@ +// +// VideoMetadata.swift +// APIExample +// +// Created by Dong Yifan on 2020/5/27. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import Foundation +import UIKit +import AgoraRtcKit +import AGEVideoLayout + +class VideoMetadataEntry : UIViewController +{ + @IBOutlet weak var joinButton: AGButton! + @IBOutlet weak var channelTextField: AGTextField! + let identifier = "VideoMetadata" + + override func viewDidLoad() { + super.viewDidLoad() + } + + @IBAction func doJoinPressed(sender: AGButton) { + guard let channelName = channelTextField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName":channelName] + self.navigationController?.pushViewController(newViewController, animated: true) + } +} + +class VideoMetadataMain: BaseViewController { + @IBOutlet weak var sendMetadataButton: UIButton! + + var localVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + var remoteVideo = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + @IBOutlet weak var container: AGEVideoContainer! + + var agoraKit: AgoraRtcEngineKit! + + // indicate if current instance has joined channel + var isJoined: Bool = false { + didSet { + sendMetadataButton.isHidden = !isJoined + } + } + + // video metadata to be sent later + var metadata: Data? + // metadata lenght limitation + let MAX_META_LENGTH = 1024 + + override func viewDidLoad(){ + super.viewDidLoad() + + sendMetadataButton.isHidden = true + + // layout render view + container.layoutStream(views: [localVideo, remoteVideo]) + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + // register metadata delegate and datasource + agoraKit.setMediaMetadataDataSource(self, with: .video) + agoraKit.setMediaMetadataDelegate(self, with: .video) + + guard let channelName = configs["channelName"] as? String, + let resolution = GlobalSettings.shared.getSetting(key: "resolution")?.selectedOption().value as? CGSize, + let fps = GlobalSettings.shared.getSetting(key: "fps")?.selectedOption().value as? AgoraVideoFrameRate, + let orientation = GlobalSettings.shared.getSetting(key: "orientation")?.selectedOption().value as? AgoraVideoOutputOrientationMode else {return} + + // make myself a broadcaster + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) + + // enable video module and set up video encoding configs + agoraKit.enableVideo() + agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: resolution, + frameRate: fps, + bitrate: AgoraVideoBitrateStandard, + orientationMode: orientation)) + + // set up local video to render your local camera preview + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + // the view to be binded + videoCanvas.view = localVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channelName, info: nil, uid: 0, options: option) + if(result != 0) { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + if isJoined { + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + } + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + view.endEditing(true) + } + + /// callback when send metadata button hit + @IBAction func onSendMetadata() { + self.metadata = "\(Date())".data(using: .utf8) + } + +} + +/// agora rtc engine delegate events +extension VideoMetadataMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // Only one remote video view is available for this + // tutorial. Here we check if there exists a surface + // view tagged as this uid. + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + } +} + +/// AgoraMediaMetadataDelegate and AgoraMediaMetadataDataSource +extension VideoMetadataMain : AgoraMediaMetadataDelegate, AgoraMediaMetadataDataSource { + func metadataMaxSize() -> Int { + // the data to send should not exceed this size + return MAX_META_LENGTH + } + + /// Callback when the SDK is ready to send metadata. + /// You need to specify the metadata in the return value of this method. + /// Ensure that the size of the metadata that you specify in this callback does not exceed the value set in the metadataMaxSize callback. + /// @param timestamp The timestamp (ms) of the current metadata. + /// @return The metadata that you want to send in the format of Data + func readyToSendMetadata(atTimestamp timestamp: TimeInterval) -> Data? { + guard let metadata = self.metadata else {return nil} + + // clear self.metadata to nil after any success send to avoid redundancy + self.metadata = nil + + if(metadata.count > MAX_META_LENGTH) { + //if data exceeding limit, return nil to not send anything + LogUtils.log(message: "invalid metadata: length exceeds \(MAX_META_LENGTH)", level: .info) + return nil + } + LogUtils.log(message: "metadata sent", level: .info) + self.metadata = nil + return metadata + } + + /// Callback when the local user receives the metadata. + /// @param data The received metadata. + /// @param uid The ID of the user who sends the metadata. + /// @param timestamp The timestamp (ms) of the received metadata. + func receiveMetadata(_ data: Data, fromUser uid: Int, atTimestamp timestamp: TimeInterval) { + DispatchQueue.main.async { + LogUtils.log(message: "metadata received", level: .info) + let alert = UIAlertController(title: "Metadata received", message: String(data: data, encoding: .utf8), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) + self.present(alert, animated: true, completion: nil) + } + } + +} diff --git a/iOS/APIExample/Examples/Advanced/VideoMetadata/zh-Hans.lproj/VideoMetadata.strings b/iOS/APIExample/Examples/Advanced/VideoMetadata/zh-Hans.lproj/VideoMetadata.strings new file mode 100644 index 000000000..a77825441 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/VideoMetadata/zh-Hans.lproj/VideoMetadata.strings @@ -0,0 +1,9 @@ + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; + +/* Class = "UIButton"; normalTitle = "Send metadata"; ObjectID = "ucb-dZ-rMR"; */ +"ucb-dZ-rMR.normalTitle" = "发送SEI消息"; diff --git a/iOS/APIExample/Examples/Advanced/VoiceChanger/Base.lproj/VoiceChanger.storyboard b/iOS/APIExample/Examples/Advanced/VoiceChanger/Base.lproj/VoiceChanger.storyboard new file mode 100644 index 000000000..3549e8860 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/VoiceChanger/Base.lproj/VoiceChanger.storyboard @@ -0,0 +1,386 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Advanced/VoiceChanger/VoiceChanger.swift b/iOS/APIExample/Examples/Advanced/VoiceChanger/VoiceChanger.swift new file mode 100644 index 000000000..a5e4340f4 --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/VoiceChanger/VoiceChanger.swift @@ -0,0 +1,481 @@ +// +// VoiceChanger.swift +// APIExample +// +// Created by 张乾泽 on 2020/7/24. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import Foundation +import UIKit +import AgoraRtcKit +import AGEVideoLayout + + +class VoiceChangerEntry : UIViewController +{ + @IBOutlet weak var joinButton: AGButton! + @IBOutlet weak var channelTextField: AGTextField! + let identifier = "VoiceChanger" + + override func viewDidLoad() { + super.viewDidLoad() + } + + @IBAction func doJoinPressed(sender: AGButton) { + guard let channelName = channelTextField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName":channelName] + self.navigationController?.pushViewController(newViewController, animated: true) + } +} + +class VoiceChangerMain: BaseViewController { + var agoraKit: AgoraRtcEngineKit! + @IBOutlet weak var chatBeautifierBtn: UIButton! + @IBOutlet weak var timbreTransformationBtn: UIButton! + @IBOutlet weak var voiceChangerBtn: UIButton! + @IBOutlet weak var styleTransformationBtn: UIButton! + @IBOutlet weak var roomAcousticsBtn: UIButton! + @IBOutlet weak var pitchCorrectionBtn: UIButton! + @IBOutlet weak var equalizationFreqBtn: UIButton! + @IBOutlet weak var reverbKeyBtn: UIButton! + @IBOutlet weak var reverbValueSlider: UISlider! + @IBOutlet weak var audioEffectParam1Slider: UISlider! + @IBOutlet weak var audioEffectParam2Slider: UISlider! + @IBOutlet weak var audioEffectParam1Label: UILabel! + @IBOutlet weak var audioEffectParam2Label: UILabel! + @IBOutlet weak var container: AGEVideoContainer! + var audioViews: [UInt:VideoView] = [:] + var equalizationFreq: AgoraAudioEqualizationBandFrequency = .band31 + var equalizationGain: Int = 0 + var reverbType: AgoraAudioReverbType = .dryLevel + var reverbMap:[AgoraAudioReverbType:Int] = [ + .dryLevel:0, + .wetLevel:0, + .roomSize:0, + .wetDelay:0, + .strength:0 + ] + var currentAudioEffects:AgoraAudioEffectPreset = .audioEffectOff + + // indicate if current instance has joined channel + var isJoined: Bool = false + + func resetVoiceChanger() { + chatBeautifierBtn.setTitle("Off", for: .normal) + timbreTransformationBtn.setTitle("Off", for: .normal) + voiceChangerBtn.setTitle("Off", for: .normal) + styleTransformationBtn.setTitle("Off", for: .normal) + roomAcousticsBtn.setTitle("Off", for: .normal) + pitchCorrectionBtn.setTitle("Off", for: .normal) + } + + func updateAudioEffectsControls(_ effect:AgoraAudioEffectPreset) { + currentAudioEffects = effect + if(effect == .roomAcoustics3DVoice) { + audioEffectParam1Slider.isEnabled = true + audioEffectParam2Slider.isEnabled = false + audioEffectParam1Label.text = "Cycle" + audioEffectParam2Label.text = "N/A" + audioEffectParam1Slider.minimumValue = 0 + audioEffectParam1Slider.maximumValue = 60 + audioEffectParam1Slider.value = 10 + } else if(effect == .pitchCorrection) { + audioEffectParam1Slider.isEnabled = true + audioEffectParam2Slider.isEnabled = true + audioEffectParam1Label.text = "Tonic Mode" + audioEffectParam2Label.text = "Tonic Pitch" + + audioEffectParam1Slider.minimumValue = 1 + audioEffectParam1Slider.maximumValue = 3 + audioEffectParam1Slider.value = 1 + audioEffectParam2Slider.minimumValue = 1 + audioEffectParam2Slider.maximumValue = 12 + audioEffectParam2Slider.value = 4 + } else { + audioEffectParam1Slider.isEnabled = false + audioEffectParam2Slider.isEnabled = false + audioEffectParam1Label.text = "N/A" + audioEffectParam2Label.text = "N/A" + } + } + + func getChatBeautifierAction(_ chatBeautifier:AgoraVoiceBeautifierPreset) -> UIAlertAction{ + return UIAlertAction(title: "\(chatBeautifier.description())", style: .default, handler: {[unowned self] action in + self.resetVoiceChanger() + self.updateAudioEffectsControls(.audioEffectOff) + //when using this method with setLocalVoiceReverbPreset, + //the method called later overrides the one called earlier + self.agoraKit.setVoiceBeautifierPreset(chatBeautifier) + self.chatBeautifierBtn.setTitle("\(chatBeautifier.description())", for: .normal) + }) + } + + func getTimbreTransformationAction(_ timbreTransformation:AgoraVoiceBeautifierPreset) -> UIAlertAction{ + return UIAlertAction(title: "\(timbreTransformation.description())", style: .default, handler: {[unowned self] action in + self.resetVoiceChanger() + self.updateAudioEffectsControls(.audioEffectOff) + //when using this method with setLocalVoiceReverbPreset, + //the method called later overrides the one called earlier + self.agoraKit.setVoiceBeautifierPreset(timbreTransformation) + self.timbreTransformationBtn.setTitle("\(timbreTransformation.description())", for: .normal) + }) + } + + func getVoiceChangerAction(_ voiceChanger:AgoraAudioEffectPreset) -> UIAlertAction{ + return UIAlertAction(title: "\(voiceChanger.description())", style: .default, handler: {[unowned self] action in + self.resetVoiceChanger() + self.updateAudioEffectsControls(voiceChanger) + //when using this method with setLocalVoiceReverbPreset, + //the method called later overrides the one called earlier + self.agoraKit.setAudioEffectPreset(voiceChanger) + self.voiceChangerBtn.setTitle("\(voiceChanger.description())", for: .normal) + }) + } + + func getStyleTransformationAction(_ styleTransformation:AgoraAudioEffectPreset) -> UIAlertAction{ + return UIAlertAction(title: "\(styleTransformation.description())", style: .default, handler: {[unowned self] action in + self.resetVoiceChanger() + self.updateAudioEffectsControls(styleTransformation) + //when using this method with setLocalVoiceChanger, + //the method called later overrides the one called earlier + self.agoraKit.setAudioEffectPreset(styleTransformation) + self.styleTransformationBtn.setTitle("\(styleTransformation.description())", for: .normal) + }) + } + + func getRoomAcousticsAction(_ roomAcoustics:AgoraAudioEffectPreset) -> UIAlertAction{ + return UIAlertAction(title: "\(roomAcoustics.description())", style: .default, handler: {[unowned self] action in + self.resetVoiceChanger() + self.updateAudioEffectsControls(roomAcoustics) + //when using this method with setLocalVoiceReverbPreset, + //the method called later overrides the one called earlier + self.agoraKit.setAudioEffectPreset(roomAcoustics) + self.roomAcousticsBtn.setTitle("\(roomAcoustics.description())", for: .normal) + }) + } + + func getPitchCorrectionAction(_ pitchCorrection:AgoraAudioEffectPreset) -> UIAlertAction{ + return UIAlertAction(title: "\(pitchCorrection.description())", style: .default, handler: {[unowned self] action in + self.resetVoiceChanger() + self.updateAudioEffectsControls(pitchCorrection) + //when using this method with setLocalVoiceReverbPreset, + //the method called later overrides the one called earlier + self.agoraKit.setAudioEffectPreset(pitchCorrection) + self.pitchCorrectionBtn.setTitle("\(pitchCorrection.description())", for: .normal) + }) + } + + func getEqualizationFreqAction(_ freq:AgoraAudioEqualizationBandFrequency) -> UIAlertAction { + return UIAlertAction(title: "\(freq.description())", style: .default, handler: {[unowned self] action in + self.equalizationFreq = freq + self.equalizationFreqBtn.setTitle("\(freq.description())", for: .normal) + LogUtils.log(message: "onLocalVoiceEqualizationGain \(self.equalizationFreq.description()) \(self.equalizationGain)", level: .info) + self.agoraKit.setLocalVoiceEqualizationOf(self.equalizationFreq, withGain: self.equalizationGain) + }) + } + + func getReverbKeyAction(_ reverbType:AgoraAudioReverbType) -> UIAlertAction { + return UIAlertAction(title: "\(reverbType.description())", style: .default, handler: {[unowned self] action in + self.updateReverbValueRange(reverbKey: reverbType) + self.reverbKeyBtn.setTitle("\(reverbType.description())", for: .normal) + }) + } + + /// callback when voice changer button hit + @IBAction func onChatBeautifier() { + let alert = UIAlertController(title: "Set Chat Beautifier".localized, message: nil, preferredStyle: .actionSheet) + alert.addAction(getChatBeautifierAction(.voiceBeautifierOff)) + alert.addAction(getChatBeautifierAction(.chatBeautifierFresh)) + alert.addAction(getChatBeautifierAction(.chatBeautifierVitality)) + alert.addAction(getChatBeautifierAction(.chatBeautifierMagnetic)) + alert.addCancelAction() + present(alert, animated: true, completion: nil) + } + + /// callback when voice changer button hit + @IBAction func onTimbreTransformation() { + let alert = UIAlertController(title: "Set Timbre Transformation".localized, message: nil, preferredStyle: .actionSheet) + alert.addAction(getTimbreTransformationAction(.voiceBeautifierOff)) + alert.addAction(getTimbreTransformationAction(.timbreTransformationVigorous)) + alert.addAction(getTimbreTransformationAction(.timbreTransformationDeep)) + alert.addAction(getTimbreTransformationAction(.timbreTransformationMellow)) + alert.addAction(getTimbreTransformationAction(.timbreTransformationFalsetto)) + alert.addAction(getTimbreTransformationAction(.timbreTransformationFull)) + alert.addAction(getTimbreTransformationAction(.timbreTransformationClear)) + alert.addAction(getTimbreTransformationAction(.timbreTransformationResounding)) + alert.addAction(getTimbreTransformationAction(.timbreTransformationRinging)) + alert.addCancelAction() + present(alert, animated: true, completion: nil) + } + + /// callback when voice changer button hit + @IBAction func onVoiceChanger() { + let alert = UIAlertController(title: "Set Voice Changer".localized, message: nil, preferredStyle: .actionSheet) + alert.addAction(getVoiceChangerAction(.audioEffectOff)) + alert.addAction(getVoiceChangerAction(.voiceChangerEffectUncle)) + alert.addAction(getVoiceChangerAction(.voiceChangerEffectOldMan)) + alert.addAction(getVoiceChangerAction(.voiceChangerEffectBoy)) + alert.addAction(getVoiceChangerAction(.voiceChangerEffectSister)) + alert.addAction(getVoiceChangerAction(.voiceChangerEffectGirl)) + alert.addAction(getVoiceChangerAction(.voiceChangerEffectPigKing)) + alert.addAction(getVoiceChangerAction(.voiceChangerEffectHulk)) + alert.addCancelAction() + present(alert, animated: true, completion: nil) + } + + /// callback when voice changer button hit + @IBAction func onStyleTransformation() { + let alert = UIAlertController(title: "Set Style Transformation".localized, message: nil, preferredStyle: .actionSheet) + alert.addAction(getStyleTransformationAction(.audioEffectOff)) + alert.addAction(getStyleTransformationAction(.styleTransformationPopular)) + alert.addAction(getStyleTransformationAction(.styleTransformationRnB)) + alert.addCancelAction() + present(alert, animated: true, completion: nil) + } + + /// callback when voice changer button hit + @IBAction func onRoomAcoustics() { + let alert = UIAlertController(title: "Set Room Acoustics".localized, message: nil, preferredStyle: .actionSheet) + alert.addAction(getRoomAcousticsAction(.roomAcousticsKTV)) + alert.addAction(getRoomAcousticsAction(.roomAcousticsVocalConcert)) + alert.addAction(getRoomAcousticsAction(.roomAcousticsStudio)) + alert.addAction(getRoomAcousticsAction(.roomAcousticsPhonograph)) + alert.addAction(getRoomAcousticsAction(.roomAcousticsVirtualStereo)) + alert.addAction(getRoomAcousticsAction(.roomAcousticsSpacial)) + alert.addAction(getRoomAcousticsAction(.roomAcousticsEthereal)) + alert.addAction(getRoomAcousticsAction(.roomAcoustics3DVoice)) + alert.addCancelAction() + present(alert, animated: true, completion: nil) + } + + /// callback when voice changer button hit + @IBAction func onPitchCorrection() { + let alert = UIAlertController(title: "Set Pitch Correction".localized, message: nil, preferredStyle: .actionSheet) + alert.addAction(getPitchCorrectionAction(.pitchCorrection)) + alert.addCancelAction() + present(alert, animated: true, completion: nil) + } + + @IBAction func onAudioEffectsParamUpdated(_ sender: UISlider) { + let param1 = audioEffectParam1Slider.isEnabled ? Int32(audioEffectParam1Slider.value) : 0 + let param2 = audioEffectParam2Slider.isEnabled ? Int32(audioEffectParam2Slider.value) : 0 + LogUtils.log(message: "onAudioEffectsParamUpdated \(currentAudioEffects.description()) \(param1) \(param2)", level: .info) + agoraKit.setAudioEffectParameters(currentAudioEffects, param1: param1, param2: param2) + } + + @IBAction func onLocalVoicePitch(_ sender:UISlider) { + LogUtils.log(message: "onLocalVoicePitch \(Double(sender.value))", level: .info) + agoraKit.setLocalVoicePitch(Double(sender.value)) + } + + @IBAction func onLocalVoiceEqualizaitonFreq(_ sender:UIButton) { + let alert = UIAlertController(title: "Set Band Frequency".localized, message: nil, preferredStyle: .actionSheet) + alert.addAction(getEqualizationFreqAction(.band31)) + alert.addAction(getEqualizationFreqAction(.band62)) + alert.addAction(getEqualizationFreqAction(.band125)) + alert.addAction(getEqualizationFreqAction(.band250)) + alert.addAction(getEqualizationFreqAction(.band500)) + alert.addAction(getEqualizationFreqAction(.band1K)) + alert.addAction(getEqualizationFreqAction(.band2K)) + alert.addAction(getEqualizationFreqAction(.band4K)) + alert.addAction(getEqualizationFreqAction(.band8K)) + alert.addAction(getEqualizationFreqAction(.band16K)) + alert.addCancelAction() + present(alert, animated: true, completion: nil) + } + + @IBAction func onLocalVoiceEqualizationGain(_ sender:UISlider) { + equalizationGain = Int(sender.value) + LogUtils.log(message: "onLocalVoiceEqualizationGain \(equalizationFreq.description()) \(equalizationGain)", level: .info) + agoraKit.setLocalVoiceEqualizationOf(equalizationFreq, withGain: equalizationGain) + } + + func updateReverbValueRange(reverbKey:AgoraAudioReverbType) { + var min:Float = 0, max:Float = 0 + switch reverbKey { + case .dryLevel: + min = -20 + max = 10 + break + case .wetLevel: + min = -20 + max = 10 + break + case .roomSize: + min = 0 + max = 100 + break + case .wetDelay: + min = 0 + max = 200 + break + case .strength: + min = 0 + max = 100 + break + default: break + } + reverbValueSlider.minimumValue = min + reverbValueSlider.maximumValue = max + reverbValueSlider.value = Float(reverbMap[reverbType] ?? 0) + } + + @IBAction func onLocalVoiceReverbKey(_ sender:UIButton) { + let alert = UIAlertController(title: "Set Reverb Key".localized, message: nil, preferredStyle: .actionSheet) + alert.addAction(getReverbKeyAction(.dryLevel)) + alert.addAction(getReverbKeyAction(.wetLevel)) + alert.addAction(getReverbKeyAction(.roomSize)) + alert.addAction(getReverbKeyAction(.wetDelay)) + alert.addAction(getReverbKeyAction(.strength)) + alert.addCancelAction() + present(alert, animated: true, completion: nil) + } + + @IBAction func onLocalVoiceReverbValue(_ sender:UISlider) { + let value = Int(sender.value) + reverbMap[reverbType] = value + LogUtils.log(message: "onLocalVoiceReverbValue \(reverbType.description()) \(value)", level: .info) + agoraKit.setLocalVoiceReverbOf(reverbType, withValue: value) + } + + override func viewDidLoad(){ + super.viewDidLoad() + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + guard let channelName = configs["channelName"] as? String else {return} + self.title = channelName + + // reset voice changer options + resetVoiceChanger() + equalizationFreqBtn.setTitle("\(equalizationFreq.description())", for: .normal) + reverbKeyBtn.setTitle("\(reverbType.description())", for: .normal) + + // Before calling the method, you need to set the profile + // parameter of setAudioProfile to AUDIO_PROFILE_MUSIC_HIGH_QUALITY(4) + // or AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO(5), and to set + // scenario parameter to AUDIO_SCENARIO_GAME_STREAMING(3). + agoraKit.setAudioProfile(.musicHighQualityStereo, scenario: .gameStreaming) + + // make myself a broadcaster + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) + + // disable video module + agoraKit.disableVideo() + + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channelName, info: nil, uid: 0, options: option) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + if isJoined { + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + } + } +} + +/// agora rtc engine delegate events +extension VoiceChangerMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + + //set up local audio view, this view will not show video but just a placeholder + let view = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + audioViews[uid] = view + view.setPlaceholder(text: self.getAudioLabel(uid: uid, isLocal: true)) + container.layoutStream2x1(views: Array(self.audioViews.values)) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + //set up remote audio view, this view will not show video but just a placeholder + let view = Bundle.loadView(fromNib: "VideoView", withType: VideoView.self) + self.audioViews[uid] = view + view.setPlaceholder(text: self.getAudioLabel(uid: uid, isLocal: false)) + self.container.layoutStream2x1(views: Array(self.audioViews.values)) + self.container.reload(level: 0, animated: true) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + //remove remote audio view + self.audioViews.removeValue(forKey: uid) + self.container.layoutStream2x1(views: Array(self.audioViews.values)) + self.container.reload(level: 0, animated: true) + } +} diff --git a/iOS/APIExample/Examples/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings b/iOS/APIExample/Examples/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings new file mode 100644 index 000000000..0f019c64e --- /dev/null +++ b/iOS/APIExample/Examples/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings @@ -0,0 +1,51 @@ + +/* Class = "UILabel"; text = "BandGain"; ObjectID = "1Y2-Oo-uXg"; */ +"1Y2-Oo-uXg.text" = "波段区间增益"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "3nP-Fh-fTT"; */ +"3nP-Fh-fTT.normalTitle" = "Button"; + +/* Class = "UILabel"; text = "ReverbValue"; ObjectID = "5YF-rQ-jpm"; */ +"5YF-rQ-jpm.text" = "混响值"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "9l7-uP-OVe"; */ +"9l7-uP-OVe.normalTitle" = "Button"; + +/* Class = "UILabel"; text = "ReverbKey"; ObjectID = "CNw-fg-lwN"; */ +"CNw-fg-lwN.text" = "混响属性"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "Cnn-JW-gFk"; */ +"Cnn-JW-gFk.normalTitle" = "Button"; + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UILabel"; text = "Customize Voice Effects"; ObjectID = "Glv-Ys-lDb"; */ +"Glv-Ys-lDb.text" = "自定义音效"; + +/* Class = "UIButton"; normalTitle = "Change Voice"; ObjectID = "IgY-un-YoE"; */ +"IgY-un-YoE.normalTitle" = "变声"; + +/* Class = "UIViewController"; title = "VoiceChanger"; ObjectID = "Isk-Yn-Sjt"; */ +"Isk-Yn-Sjt.title" = "美声/音效"; + +/* Class = "UILabel"; text = "Pitch"; ObjectID = "J9L-hL-6cu"; */ +"J9L-hL-6cu.text" = "音调"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "Ry4-kh-SCQ"; */ +"Ry4-kh-SCQ.normalTitle" = "Button"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "bHF-lw-k2f"; */ +"bHF-lw-k2f.normalTitle" = "Button"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "dJC-qv-NZE"; */ +"dJC-qv-NZE.normalTitle" = "Button"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; + +/* Class = "UILabel"; text = "BandFreq"; ObjectID = "m8A-hY-ceu"; */ +"m8A-hY-ceu.text" = "波段频率"; + +/* Class = "UILabel"; text = "Voice Beautifier & Effects Preset"; ObjectID = "qQS-En-EUC"; */ +"qQS-En-EUC.text" = "美声/音效预设"; diff --git a/iOS/APIExample/Examples/Basic/JoinChannelAudio/Base.lproj/JoinChannelAudio.storyboard b/iOS/APIExample/Examples/Basic/JoinChannelAudio/Base.lproj/JoinChannelAudio.storyboard new file mode 100644 index 000000000..70f1b77b2 --- /dev/null +++ b/iOS/APIExample/Examples/Basic/JoinChannelAudio/Base.lproj/JoinChannelAudio.storyboard @@ -0,0 +1,289 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift b/iOS/APIExample/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift new file mode 100644 index 000000000..9d0487ab6 --- /dev/null +++ b/iOS/APIExample/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift @@ -0,0 +1,274 @@ +// +// JoinChannelAudioMain.swift +// APIExample +// +// Created by ADMIN on 2020/5/18. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import UIKit +import AgoraRtcKit +import AGEVideoLayout + +class JoinChannelAudioEntry: UIViewController { + @IBOutlet weak var joinButton: AGButton! + @IBOutlet weak var channelTextField: AGTextField! + @IBOutlet weak var scenarioBtn: UIButton! + @IBOutlet weak var profileBtn: UIButton! + var profile:AgoraAudioProfile = .default + var scenario:AgoraAudioScenario = .default + let identifier = "JoinChannelAudio" + + override func viewDidLoad() { + super.viewDidLoad() + + profileBtn.setTitle("\(profile.description())", for: .normal) + scenarioBtn.setTitle("\(scenario.description())", for: .normal) + } + + @IBAction func doJoinPressed(sender: AGButton) { + guard let channelName = channelTextField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else {return} + newViewController.title = channelName + newViewController.configs = ["channelName":channelName, "audioProfile":profile, "audioScenario":scenario] + self.navigationController?.pushViewController(newViewController, animated: true) + } + + func getAudioProfileAction(_ profile:AgoraAudioProfile) -> UIAlertAction { + return UIAlertAction(title: "\(profile.description())", style: .default, handler: {[unowned self] action in + self.profile = profile + self.profileBtn.setTitle("\(profile.description())", for: .normal) + }) + } + + func getAudioScenarioAction(_ scenario:AgoraAudioScenario) -> UIAlertAction { + return UIAlertAction(title: "\(scenario.description())", style: .default, handler: {[unowned self] action in + self.scenario = scenario + self.scenarioBtn.setTitle("\(scenario.description())", for: .normal) + }) + } + + @IBAction func setAudioProfile() { + let alert = UIAlertController(title: "Set Audio Profile".localized, message: nil, preferredStyle: .actionSheet) + for profile in AgoraAudioProfile.allValues(){ + alert.addAction(getAudioProfileAction(profile)) + } + alert.addCancelAction() + present(alert, animated: true, completion: nil) + } + + @IBAction func setAudioScenario() { + let alert = UIAlertController(title: "Set Audio Scenario".localized, message: nil, preferredStyle: .actionSheet) + for scenario in AgoraAudioScenario.allValues(){ + alert.addAction(getAudioScenarioAction(scenario)) + } + alert.addCancelAction() + present(alert, animated: true, completion: nil) + } +} + +class JoinChannelAudioMain: BaseViewController { + var agoraKit: AgoraRtcEngineKit! + @IBOutlet weak var container: AGEVideoContainer! + @IBOutlet weak var recordingVolumeSlider: UISlider! + @IBOutlet weak var playbackVolumeSlider: UISlider! + @IBOutlet weak var inEarMonitoringSwitch: UISwitch! + @IBOutlet weak var inEarMonitoringVolumeSlider: UISlider! + var audioViews: [UInt:VideoView] = [:] + + // indicate if current instance has joined channel + var isJoined: Bool = false + + override func viewDidLoad(){ + super.viewDidLoad() + + // set up agora instance when view loadedlet config = AgoraRtcEngineConfig() + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + guard let channelName = configs["channelName"] as? String, + let audioProfile = configs["audioProfile"] as? AgoraAudioProfile, + let audioScenario = configs["audioScenario"] as? AgoraAudioScenario + else {return} + + // make myself a broadcaster + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) + + // disable video module + agoraKit.disableVideo() + + // set audio profile/audio scenario + agoraKit.setAudioProfile(audioProfile, scenario: audioScenario) + + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + + // enable volume indicator + agoraKit.enableAudioVolumeIndication(200, smooth: 3, report_vad: false) + + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channelName, info: nil, uid: 0, options: option) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + if isJoined { + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + } + } + + func sortedViews() -> [VideoView] { + return Array(audioViews.values).sorted(by: { $0.uid < $1.uid }) + } + + @IBAction func onChangeRecordingVolume(_ sender:UISlider){ + let value:Int = Int(sender.value) + print("adjustRecordingSignalVolume \(value)") + agoraKit.adjustRecordingSignalVolume(value) + } + + @IBAction func onChangePlaybackVolume(_ sender:UISlider){ + let value:Int = Int(sender.value) + print("adjustPlaybackSignalVolume \(value)") + agoraKit.adjustPlaybackSignalVolume(value) + } + + @IBAction func toggleInEarMonitoring(_ sender:UISwitch){ + inEarMonitoringVolumeSlider.isEnabled = sender.isOn + agoraKit.enable(inEarMonitoring: sender.isOn) + } + + @IBAction func onChangeInEarMonitoringVolume(_ sender:UISlider){ + let value:Int = Int(sender.value) + print("setInEarMonitoringVolume \(value)") + agoraKit.setInEarMonitoringVolume(value) + } +} + +/// agora rtc engine delegate events +extension JoinChannelAudioMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + + //set up local audio view, this view will not show video but just a placeholder + let view = Bundle.loadVideoView(type: .local, audioOnly: true) + audioViews[0] = view + view.setPlaceholder(text: self.getAudioLabel(uid: uid, isLocal: true)) + container.layoutStream3x2(views: self.sortedViews()) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + //set up remote audio view, this view will not show video but just a placeholder + let view = Bundle.loadVideoView(type: .remote, audioOnly: true) + view.uid = uid + self.audioViews[uid] = view + view.setPlaceholder(text: self.getAudioLabel(uid: uid, isLocal: false)) + self.container.layoutStream3x2(views: sortedViews()) + self.container.reload(level: 0, animated: true) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + //remove remote audio view + self.audioViews.removeValue(forKey: uid) + self.container.layoutStream3x2(views: sortedViews()) + self.container.reload(level: 0, animated: true) + } + + /// Reports which users are speaking, the speakers' volumes, and whether the local user is speaking. + /// @params speakers volume info for all speakers + /// @params totalVolume Total volume after audio mixing. The value range is [0,255]. + func rtcEngine(_ engine: AgoraRtcEngineKit, reportAudioVolumeIndicationOfSpeakers speakers: [AgoraRtcAudioVolumeInfo], totalVolume: Int) { + for volumeInfo in speakers { + if let audioView = audioViews[volumeInfo.uid] { + audioView.setInfo(text: "Volume:\(volumeInfo.volume)") + } + } + } + + /// Reports the statistics of the current call. The SDK triggers this callback once every two seconds after the user joins the channel. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, reportRtcStats stats: AgoraChannelStats) { + audioViews[0]?.statsInfo?.updateChannelStats(stats) + } + + /// Reports the statistics of the uploading local audio streams once every two seconds. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, localAudioStats stats: AgoraRtcLocalAudioStats) { + audioViews[0]?.statsInfo?.updateLocalAudioStats(stats) + } + + /// Reports the statistics of the audio stream from each remote user/host. + /// @param stats stats struct for current call statistics + func rtcEngine(_ engine: AgoraRtcEngineKit, remoteAudioStats stats: AgoraRtcRemoteAudioStats) { + audioViews[stats.uid]?.statsInfo?.updateAudioStats(stats) + } +} diff --git a/iOS/APIExample/Examples/Basic/JoinChannelAudio/zh-Hans.lproj/JoinChannelAudio.strings b/iOS/APIExample/Examples/Basic/JoinChannelAudio/zh-Hans.lproj/JoinChannelAudio.strings new file mode 100644 index 000000000..b42ff128a --- /dev/null +++ b/iOS/APIExample/Examples/Basic/JoinChannelAudio/zh-Hans.lproj/JoinChannelAudio.strings @@ -0,0 +1,33 @@ + +/* Class = "UILabel"; text = "PlaybackVolume"; ObjectID = "07c-He-s8j"; */ +"07c-He-s8j.text" = "播放音量"; + +/* Class = "UILabel"; text = "RecordingVolume"; ObjectID = "DJt-Y7-fkM"; */ +"DJt-Y7-fkM.text" = "采集音量"; + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UILabel"; text = "Audio Scenario"; ObjectID = "Q0E-5B-IED"; */ +"Q0E-5B-IED.text" = "音频使用场景"; + +/* Class = "UILabel"; text = "InEar Monitoring Volume"; ObjectID = "VMe-lv-SUb"; */ +"VMe-lv-SUb.text" = "耳返音量"; + +/* Class = "UILabel"; text = "Audio Profile"; ObjectID = "iUn-XK-AS2"; */ +"iUn-XK-AS2.text" = "音频参数配置"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "iZP-Ce-Oxt"; */ +"iZP-Ce-Oxt.normalTitle" = "Button"; + +/* Class = "UILabel"; text = "InEar Monitoring"; ObjectID = "iru-5f-bbo"; */ +"iru-5f-bbo.text" = "耳返"; + +/* Class = "UIViewController"; title = "Join Channel Audio"; ObjectID = "jxp-ZN-2yG"; */ +"jxp-ZN-2yG.title" = "实时音频通话/直播"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "myR-6e-1zj"; */ +"myR-6e-1zj.normalTitle" = "Button"; diff --git a/iOS/APIExample/Examples/Basic/JoinChannelVideo/Base.lproj/JoinChannelVideo.storyboard b/iOS/APIExample/Examples/Basic/JoinChannelVideo/Base.lproj/JoinChannelVideo.storyboard new file mode 100644 index 000000000..fecbab976 --- /dev/null +++ b/iOS/APIExample/Examples/Basic/JoinChannelVideo/Base.lproj/JoinChannelVideo.storyboard @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/APIExample/Examples/Basic/JoinChannelVideo/JoinChannelVideo.swift b/iOS/APIExample/Examples/Basic/JoinChannelVideo/JoinChannelVideo.swift new file mode 100644 index 000000000..f80599189 --- /dev/null +++ b/iOS/APIExample/Examples/Basic/JoinChannelVideo/JoinChannelVideo.swift @@ -0,0 +1,220 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import UIKit +import AGEVideoLayout +import AgoraRtcKit + +class JoinChannelVideoEntry : UIViewController +{ + @IBOutlet weak var joinButton: UIButton! + @IBOutlet weak var channelTextField: UITextField! + + let identifier = "JoinChannelVideo" + + override func viewDidLoad() { + super.viewDidLoad() + } + + @IBAction func doJoinPressed(sender: UIButton) { + guard let channelName = channelTextField.text else {return} + //resign channel text field + channelTextField.resignFirstResponder() + + let storyBoard: UIStoryboard = UIStoryboard(name: identifier, bundle: nil) + // create new view controller every time to ensure we get a clean vc + guard let newViewController = storyBoard.instantiateViewController(withIdentifier: identifier) as? BaseViewController else { return } + newViewController.title = channelName + newViewController.configs = ["channelName": channelName] + self.navigationController?.pushViewController(newViewController, animated: true) + } +} + +class JoinChannelVideoMain: BaseViewController { + var localVideo = Bundle.loadVideoView(type: .local, audioOnly: false) + var remoteVideo = Bundle.loadVideoView(type: .remote, audioOnly: false) + + @IBOutlet weak var container: AGEVideoContainer! + var agoraKit: AgoraRtcEngineKit! + + // indicate if current instance has joined channel + var isJoined: Bool = false + + override func viewDidLoad() { + super.viewDidLoad() + // layout render view + localVideo.setPlaceholder(text: "Local Host".localized) + remoteVideo.setPlaceholder(text: "Remote Host".localized) + container.layoutStream(views: [localVideo, remoteVideo]) + + // set up agora instance when view loaded + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + + // setup log file path + let logConfig = AgoraLogConfig() + logConfig.filePath = LogUtils.sdkLogPath() + config.logConfig = logConfig + + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + // get channel name from configs + guard let channelName = configs["channelName"] as? String, + let resolution = GlobalSettings.shared.getSetting(key: "resolution")?.selectedOption().value as? CGSize, + let fps = GlobalSettings.shared.getSetting(key: "fps")?.selectedOption().value as? AgoraVideoFrameRate, + let orientation = GlobalSettings.shared.getSetting(key: "orientation")?.selectedOption().value as? AgoraVideoOutputOrientationMode else {return} + + // make myself a broadcaster + agoraKit.setChannelProfile(.liveBroadcasting) + agoraKit.setClientRole(.broadcaster) + + // enable video module and set up video encoding configs + agoraKit.enableVideo() + agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: resolution, + frameRate: fps, + bitrate: AgoraVideoBitrateStandard, + orientationMode: orientation)) + + // set up local video to render your local camera preview + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + // the view to be binded + videoCanvas.view = localVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + + // Set audio route to speaker + agoraKit.setDefaultAudioRouteToSpeakerphone(true) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channelName, info: nil, uid: 0, options: option) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } + + override func willMove(toParent parent: UIViewController?) { + if parent == nil { + // leave channel when exiting the view + if isJoined { + agoraKit.leaveChannel { (stats) -> Void in + LogUtils.log(message: "left channel, duration: \(stats.duration)", level: .info) + } + } + } + } +} + +/// agora rtc engine delegate events +extension JoinChannelVideoMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.description)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.description) occur") + agoraKit.uploadLogFile() + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // Only one remote video view is available for this + // tutorial. Here we check if there exists a surface + // view tagged as this uid. + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videoView + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + } + + /// Reports the statistics of the current call. The SDK triggers this callback once every two seconds after the user joins the channel. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, reportRtcStats stats: AgoraChannelStats) { + localVideo.statsInfo?.updateChannelStats(stats) + } + + /// Reports the statistics of the uploading local video streams once every two seconds. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, localVideoStats stats: AgoraRtcLocalVideoStats) { + localVideo.statsInfo?.updateLocalVideoStats(stats) + } + + /// Reports the statistics of the uploading local audio streams once every two seconds. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, localAudioStats stats: AgoraRtcLocalAudioStats) { + localVideo.statsInfo?.updateLocalAudioStats(stats) + } + + /// Reports the statistics of the video stream from each remote user/host. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, remoteVideoStats stats: AgoraRtcRemoteVideoStats) { + remoteVideo.statsInfo?.updateVideoStats(stats) + } + + /// Reports the statistics of the audio stream from each remote user/host. + /// @param stats stats struct for current call statistics + func rtcEngine(_ engine: AgoraRtcEngineKit, remoteAudioStats stats: AgoraRtcRemoteAudioStats) { + remoteVideo.statsInfo?.updateAudioStats(stats) + } +} diff --git a/iOS/APIExample/Examples/Basic/JoinChannelVideo/zh-Hans.lproj/JoinChannelVideo.strings b/iOS/APIExample/Examples/Basic/JoinChannelVideo/zh-Hans.lproj/JoinChannelVideo.strings new file mode 100644 index 000000000..25a97ee8c --- /dev/null +++ b/iOS/APIExample/Examples/Basic/JoinChannelVideo/zh-Hans.lproj/JoinChannelVideo.strings @@ -0,0 +1,21 @@ + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UINavigationItem"; title = "Join Channel"; ObjectID = "Iy0-Dq-h5x"; */ +"Iy0-Dq-h5x.title" = "加入频道"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "VpM-9W-auG"; */ +"VpM-9W-auG.normalTitle" = "Button"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "kf0-3f-UI5"; */ +"kf0-3f-UI5.normalTitle" = "Button"; + +/* Class = "UIViewController"; title = "Join Channel Video"; ObjectID = "p70-sh-D1h"; */ +"p70-sh-D1h.title" = "视频实时通话"; + +/* Class = "UIButton"; normalTitle = "Button"; ObjectID = "wHl-zh-dFe"; */ +"wHl-zh-dFe.normalTitle" = "Button"; diff --git a/iOS/APIExample/Examples/Live-Broadcasting/RTMPStreaming.swift b/iOS/APIExample/Examples/Live-Broadcasting/RTMPStreaming.swift deleted file mode 100644 index 157047149..000000000 --- a/iOS/APIExample/Examples/Live-Broadcasting/RTMPStreaming.swift +++ /dev/null @@ -1,242 +0,0 @@ -// -// JoinChannelVC.swift -// APIExample -// -// Created by 张乾泽 on 2020/4/17. -// Copyright © 2020 Agora Corp. All rights reserved. -// - -import Foundation -import UIKit -import AgoraRtcKit - -let CANVAS_WIDTH = 640 -let CANVAS_HEIGHT = 480 - -class RTMPStreamingMain: BaseViewController { - @IBOutlet weak var joinButton: UIButton! - @IBOutlet weak var channelTextField: UITextField! - @IBOutlet weak var publishButton: UIButton! - @IBOutlet weak var rtmpTextField: UITextField! - - // indicate if current instance has joined channel - var isJoined: Bool = false { - didSet { - channelTextField.isEnabled = !isJoined - joinButton.isHidden = isJoined - rtmpTextField.isHidden = !isJoined - publishButton.isHidden = !isJoined - } - } - var localVideo = VideoView(frame: CGRect.zero) - var remoteVideo = VideoView(frame: CGRect.zero) - var agoraKit: AgoraRtcEngineKit! - var remoteUid: UInt? - var rtmpURL: String? - var transcoding = AgoraLiveTranscoding.default() - - override func viewDidLoad() { - super.viewDidLoad() - // set up agora instance when view loaded - agoraKit = AgoraRtcEngineKit.sharedEngine(withAppId: KeyCenter.AppId, delegate: self) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - // leave channel when exiting the view - if(isJoined) { - if let rtmpURL = rtmpURL { - agoraKit.removePublishStreamUrl(rtmpURL) - } - - agoraKit.leaveChannel { (stats) -> Void in - LogUtils.log(msg: "left channel, duration: \(stats.duration)", level: .info) - } - } - } - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - guard let identifier = segue.identifier else { - return - } - - switch identifier { - case "RenderViewController": - let vc = segue.destination as! RenderViewController - vc.layoutStream(views: [localVideo, remoteVideo]) - default: - break - } - } - - override func touchesBegan(_ touches: Set, with event: UIEvent?) { - view.endEditing(true) - } - - /// callback when join button hit - @IBAction func onJoin() { - guard let channelName = channelTextField.text else {return} - - // resign channelTextField - channelTextField.resignFirstResponder() - - // enable video module and set up video encoding configs - agoraKit.enableVideo() - agoraKit.setVideoEncoderConfiguration(AgoraVideoEncoderConfiguration(size: AgoraVideoDimension320x240, - frameRate: .fps15, - bitrate: AgoraVideoBitrateStandard, - orientationMode: .adaptative)) - - // set up local video to render your local camera preview - let videoCanvas = AgoraRtcVideoCanvas() - videoCanvas.uid = 0 - // the view to be binded - videoCanvas.view = localVideo.videoView - videoCanvas.renderMode = .hidden - agoraKit.setupLocalVideo(videoCanvas) - - // Set audio route to speaker - agoraKit.setDefaultAudioRouteToSpeakerphone(true) - - // start joining channel - // 1. Users can only see each other after they join the - // same channel successfully using the same app id. - // 2. If app certificate is turned on at dashboard, token is needed - // when joining channel. The channel name and uid used to calculate - // the token has to match the ones used for channel join - let result = agoraKit.joinChannel(byToken: nil, channelId: channelName, info: nil, uid: 0) { [unowned self] (channel, uid, elapsed) -> Void in - self.isJoined = true - LogUtils.log(msg: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) - - // add transcoding user so the video stream will be involved - // in future RTMP Stream - let user = AgoraLiveTranscodingUser() - user.rect = CGRect(x: 0, y: 0, width: CANVAS_WIDTH / 2, height: CANVAS_HEIGHT) - user.uid = uid - self.transcoding.add(user) - } - if (result != 0) { - // Usually happens with invalid parameters - // Error code description can be found at: - // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html - // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html - self.showAlert(title: "Error", msg: "joinChannel call failed: \(result), please check your params") - } - } - - /// callback when publish button hit - @IBAction func onPublish() { - guard let rtmpURL = rtmpTextField.text else { - return - } - - // resign rtmp text field - rtmpTextField.resignFirstResponder() - - // we will use transcoding to composite multiple hosts' video - // therefore we have to create a livetranscoding object and call before addPublishStreamUrl - transcoding.size = CGSize(width: CANVAS_WIDTH, height: CANVAS_HEIGHT) - agoraKit.setLiveTranscoding(transcoding) - agoraKit.addPublishStreamUrl(rtmpURL, transcodingEnabled: true) - - self.rtmpURL = rtmpURL - } -} - -/// agora rtc engine delegate events -extension RTMPStreamingMain: AgoraRtcEngineDelegate { - /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out - /// what is happening - /// Warning code description can be found at: - /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html - /// @param warningCode warning code of the problem - func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { - LogUtils.log(msg: "warning: \(warningCode.description)", level: .warning) - } - - /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand - /// to let user know something wrong is happening - /// Error code description can be found at: - /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html - /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html - /// @param errorCode error code of the problem - func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { - LogUtils.log(msg: "error: \(errorCode.description)", level: .error) - } - - /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event - /// @param uid uid of remote joined user - /// @param elapsed time elapse since current sdk instance join the channel in ms - func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { - LogUtils.log(msg: "remote user join: \(uid) \(elapsed)ms", level: .info) - - // only one remote video view is available for this - // tutorial. Here we check if there exists a surface - // view tagged as this uid. - let videoCanvas = AgoraRtcVideoCanvas() - videoCanvas.uid = uid - // the view to be binded - videoCanvas.view = remoteVideo.videoView - videoCanvas.renderMode = .hidden - agoraKit.setupRemoteVideo(videoCanvas) - - // remove preivous user from the canvas - if let existingUid = remoteUid { - transcoding.removeUser(existingUid) - } - remoteUid = uid - - // add new user onto the canvas - let user = AgoraLiveTranscodingUser() - user.rect = CGRect(x: CANVAS_WIDTH / 2, y: 0, width: CANVAS_WIDTH / 2, height: CANVAS_HEIGHT) - user.uid = uid - self.transcoding.add(user) - // remember you need to call setLiveTranscoding again if you changed the layout - agoraKit.setLiveTranscoding(transcoding) - } - - /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event - /// @param uid uid of remote joined user - /// @param reason reason why this user left, note this event may be triggered when the remote user - /// become an audience in live broadcasting profile - func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { - LogUtils.log(msg: "remote user left: \(uid) reason \(reason.rawValue)", level: .info) - - // to unlink your view from sdk, so that your view reference will be released - // note the video will stay at its last frame, to completely remove it - // you will need to remove the EAGL sublayer from your binded view - let videoCanvas = AgoraRtcVideoCanvas() - videoCanvas.uid = uid - // the view to be binded - videoCanvas.view = nil - videoCanvas.renderMode = .hidden - agoraKit.setupRemoteVideo(videoCanvas) - - // remove user from canvas if current cohost left channel - if let existingUid = remoteUid { - transcoding.removeUser(existingUid) - } - remoteUid = nil - // remember you need to call setLiveTranscoding again if you changed the layout - agoraKit.setLiveTranscoding(transcoding) - } - - /// callback for state of rtmp streaming, for both good and bad state - /// @param url rtmp streaming url - /// @param state state of rtmp streaming - /// @param reason - func rtcEngine(_ engine: AgoraRtcEngineKit, rtmpStreamingChangedToState url: String, state: AgoraRtmpStreamingState, errorCode: AgoraRtmpStreamingErrorCode) { - LogUtils.log(msg: "rtmp streaming: \(url) state \(state.rawValue) error \(errorCode.rawValue)", level: .info) - if(state == .running) { - self.showAlert(title: "Notice", msg: "RTMP Publish Success") - } else if(state == .failure) { - self.showAlert(title: "Error", msg: "RTMP Publish Failed: \(errorCode.rawValue)") - } - } - - /// callback when live transcoding is properly updated - func rtcEngineTranscodingUpdated(_ engine: AgoraRtcEngineKit) { - LogUtils.log(msg: "live transcoding updated", level: .info) - } -} diff --git a/iOS/APIExample/Info.plist b/iOS/APIExample/Info.plist index 2cab67d68..1bee2c417 100644 --- a/iOS/APIExample/Info.plist +++ b/iOS/APIExample/Info.plist @@ -24,6 +24,8 @@ Request Camera Access NSMicrophoneUsageDescription Request Mic Access + NSPhotoLibraryAddUsageDescription + Request Photo Access UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/iOS/APIExample/Resources/audioeffect.mp3 b/iOS/APIExample/Resources/audioeffect.mp3 new file mode 100644 index 000000000..edde60d5c Binary files /dev/null and b/iOS/APIExample/Resources/audioeffect.mp3 differ diff --git a/iOS/APIExample/Resources/audiomixing.mp3 b/iOS/APIExample/Resources/audiomixing.mp3 new file mode 100644 index 000000000..0379b4d74 Binary files /dev/null and b/iOS/APIExample/Resources/audiomixing.mp3 differ diff --git a/iOS/APIExample/ViewController.swift b/iOS/APIExample/ViewController.swift index 99877d770..d6ea7cc2d 100644 --- a/iOS/APIExample/ViewController.swift +++ b/iOS/APIExample/ViewController.swift @@ -7,6 +7,7 @@ // import UIKit +import Floaty struct MenuSection { var name: String @@ -15,26 +16,76 @@ struct MenuSection { struct MenuItem { var name: String + var entry: String = "EntryViewController" + var storyboard: String = "Main" var controller: String + var note: String = "" } -class ViewController: UIViewController { +class ViewController: AGViewController { var menus:[MenuSection] = [ - MenuSection(name: "Basic Video/Audio", rows: [ - MenuItem(name: "Join a channel", controller: "JoinChannel") + MenuSection(name: "Basic", rows: [ + MenuItem(name: "Join a channel (Video)".localized, storyboard: "JoinChannelVideo", controller: ""), + MenuItem(name: "Join a channel (Audio)".localized, storyboard: "JoinChannelAudio", controller: "") ]), - MenuSection(name: "Live Broadcasting", rows: [ - MenuItem(name: "RTMP Streaming", controller: "RTMPStreaming"), - MenuItem(name: "RTMP Injection", controller: "RTMPInjection") + MenuSection(name: "Anvanced", rows: [ + MenuItem(name: "Group Video Chat".localized, storyboard: "VideoChat", controller: "VideoChat"), + MenuItem(name: "Live Streaming".localized, storyboard: "LiveStreaming", controller: "LiveStreaming"), + MenuItem(name: "RTMP Streaming".localized, storyboard: "RTMPStreaming", controller: "RTMPStreaming"), + MenuItem(name: "Media Injection".localized, storyboard: "RTMPInjection", controller: "RTMPInjection".localized), + MenuItem(name: "Video Metadata".localized, storyboard: "VideoMetadata", controller: "VideoMetadata".localized), + MenuItem(name: "Voice Changer".localized, storyboard: "VoiceChanger", controller: ""), + MenuItem(name: "Custom Audio Source".localized, storyboard: "CustomAudioSource", controller: "CustomAudioSource"), + MenuItem(name: "Custom Audio Render".localized, storyboard: "CustomAudioRender", controller: "CustomAudioRender"), + MenuItem(name: "Custom Video Source(MediaIO)".localized, storyboard: "CustomVideoSourceMediaIO", controller: "CustomVideoSourceMediaIO"), + MenuItem(name: "Custom Video Source(Push)".localized, storyboard: "CustomVideoSourcePush", controller: "CustomVideoSourcePush"), + MenuItem(name: "Custom Video Render".localized, storyboard: "CustomVideoRender", controller: "CustomVideoRender"), + MenuItem(name: "Raw Media Data".localized, storyboard: "RawMediaData", controller: "RawMediaData"), + MenuItem(name: "Quick Switch Channel".localized, controller: "QuickSwitchChannel"), + MenuItem(name: "Join Multiple Channels".localized, storyboard: "JoinMultiChannel", controller: "JoinMultiChannel"), + MenuItem(name: "Stream Encryption".localized, storyboard: "StreamEncryption", controller: ""), + MenuItem(name: "Audio Mixing".localized, storyboard: "AudioMixing", controller: ""), + MenuItem(name: "Precall Test".localized, storyboard: "PrecallTest", controller: ""), + MenuItem(name: "Screen Share".localized, storyboard: "ScreenShare", controller: ""), + MenuItem(name: "Super Resolution".localized, storyboard: "SuperResolution", controller: ""), + MenuItem(name: "Media Channel Relay".localized, storyboard: "MediaChannelRelay", controller: ""), + MenuItem(name: "Media Player".localized, storyboard: "MediaPlayer", controller: "MediaPlayer"), + MenuItem(name: "ARKit".localized, storyboard: "ARKit", controller: ""), + MenuItem(name: "Create Data Stream".localized, storyboard: "CreateDataStream", controller: ""), + MenuItem(name: "Raw Audio Data".localized, storyboard: "RawAudioData", controller: "RawAudioData"), ]), -// MenuSection(name: "Quality Metrics", rows: [ -// MenuItem(name: "Lastmile Test", controller: "Lastmile"), -// MenuItem(name: "Realtime Stats", controller: "RealtimeStats") -// ]) ] override func viewDidLoad() { super.viewDidLoad() + Floaty.global.button.addItem(title: "Send Logs", handler: {item in + LogUtils.writeAppLogsToDisk() + let activity = UIActivityViewController(activityItems: [NSURL(fileURLWithPath: LogUtils.logFolder(), isDirectory: true)], applicationActivities: nil) + UIApplication.topMostViewController?.present(activity, animated: true, completion: nil) + }) + + Floaty.global.button.addItem(title: "Clean Up", handler: {item in + LogUtils.cleanUp() + }) + Floaty.global.button.isDraggable = true + Floaty.global.show() + } + + @IBAction func onSettings(_ sender:UIBarButtonItem) { + let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil) + guard let settingsViewController = storyBoard.instantiateViewController(withIdentifier: "settings") as? SettingsViewController else { return } + + settingsViewController.settingsDelegate = self + settingsViewController.sectionNames = ["Video Configurations","Metadata"] + settingsViewController.sections = [ + [ + SettingsSelectParam(key: "resolution", label:"Resolution".localized, settingItem: GlobalSettings.shared.getSetting(key: "resolution")!, context: self), + SettingsSelectParam(key: "fps", label:"Frame Rate".localized, settingItem: GlobalSettings.shared.getSetting(key: "fps")!, context: self), + SettingsSelectParam(key: "orientation", label:"Orientation".localized, settingItem: GlobalSettings.shared.getSetting(key: "orientation")!, context: self) + ], + [SettingsLabelParam(key: "sdk_ver", label: "SDK Version", value: "v\(AgoraRtcEngineKit.getSdkVersion())")] + ] + self.navigationController?.pushViewController(settingsViewController, animated: true) } } @@ -66,9 +117,28 @@ extension ViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) - let name = "\(menus[indexPath.section].rows[indexPath.row].controller)" - let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil) - let newViewController = storyBoard.instantiateViewController(withIdentifier: name) - self.navigationController?.pushViewController(newViewController, animated: true) + let menuItem = menus[indexPath.section].rows[indexPath.row] + let storyBoard: UIStoryboard = UIStoryboard(name: menuItem.storyboard, bundle: nil) + + if(menuItem.storyboard == "Main") { + guard let entryViewController = storyBoard.instantiateViewController(withIdentifier: menuItem.entry) as? EntryViewController else { return } + + entryViewController.nextVCIdentifier = menuItem.controller + entryViewController.title = menuItem.name + entryViewController.note = menuItem.note + self.navigationController?.pushViewController(entryViewController, animated: true) + } else { + let entryViewController:UIViewController = storyBoard.instantiateViewController(withIdentifier: menuItem.entry) + self.navigationController?.pushViewController(entryViewController, animated: true) + } + } +} + +extension ViewController: SettingsViewControllerDelegate { + func didChangeValue(type: String, key: String, value: Any) { + if(type == "SettingsSelectCell") { + guard let setting = value as? SettingItem else {return} + LogUtils.log(message: "select \(setting.selectedOption().label) for \(key)", level: .info) + } } } diff --git a/iOS/APIExample/zh-Hans.lproj/Localizable.strings b/iOS/APIExample/zh-Hans.lproj/Localizable.strings new file mode 100644 index 000000000..901e3a8ca --- /dev/null +++ b/iOS/APIExample/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,113 @@ +/* + Localization.strings + APIExample + + Created by 张乾泽 on 2020/10/7. + Copyright © 2020 Agora Corp. All rights reserved. +*/ + +"Join a channel (Video)" = "实时视频通话/直播"; +"Join a channel (Audio)" = "实时语音通话/直播"; +"Live Streaming" = "RTC实时直播/主播/观众"; +"RTMP Streaming" = "RTMP旁路推流"; +"Media Injection" = "流媒体注入"; +"Video Metadata" = "SEI消息"; +"Voice Changer" = "美声/音效"; +"Custom Audio Source" = "音频自采集"; +"Custom Audio Render" = "音频自渲染"; +"Custom Video Source(MediaIO)" = "视频自采集(MediaIO)"; +"Custom Video Source(Push)" = "视频自采集(Push)"; +"Custom Video Render" = "视频自渲染(Metal)"; +"Quick Switch Channel" = "快速切换频道"; +"Join Multiple Channels" = "加入多频道"; +"Stream Encryption" = "音视频流加密"; +"Audio Mixing" = "音频文件混音"; +"Raw Media Data" = "音视频裸数据"; +"Precall Test" = "通话前网络/设备测试"; +"Media Player" = "流媒体播放器"; +"Screen Share" = "屏幕共享"; +"Super Resolution" = "超级分辨率"; +"Media Channel Relay" = "跨频道流转发"; +"Set Resolution" = "设置视频分辨率"; +"Set Fps" = "设置视频帧率"; +"Set Orientation" = "设置视频朝向"; +"Set Chat Beautifier" = "设置语聊美声"; +"Set Timbre Transformation" = "设置音色变换"; +"Set Voice Changer" = "设置变声音效"; +"Set Style Transformation" = "设置曲风音效"; +"Set Room Acoustics" = "设置空间音效"; +"Set Band Frequency" = "设置波段频率"; +"Set Reverb Key" = "设置混响属性"; +"Set Encryption Mode" = "设置加密模式"; +"fixed portrait" = "固定纵向"; +"fixed landscape" = "固定横向"; +"adaptive" = "自适应"; +"Local Host" = "本地预览"; +"Remote Host" = "远端视频"; +"Set Audio Profile" = "设置音频参数配置"; +"Set Audio Scenario" = "设置音频使用场景"; +"Default" = "默认"; +"Music Standard" = "标准音乐"; +"Music Standard Stereo" = "标准双声道音乐"; +"Music High Quality" = "高音质音乐"; +"Music High Quality Stereo" = "高音质双声道音乐"; +"Speech Standard" = "标准人声"; +"Chat Room Gaming" = "娱乐语聊房"; +"Education" = "教育"; +"Game Streaming" = "高音质语聊房"; +"Chat Room Entertainment" = "游戏开黑"; +"Show Room" = "秀场"; +"Cancel" = "取消"; +"Off" = "原声"; +"FemaleFresh" = "语聊美声: 清新(女)"; +"FemaleVitality" = "语聊美声: 活力(女)"; +"MaleMagnetic" = "语聊美声: 磁性(男)"; +"Vigorous" = "浑厚"; +"Deep" = "低沉"; +"Mellow" = "圆润"; +"Falsetto" = "假音"; +"Full" = "饱满"; +"Clear" = "清澈"; +"Resounding" = "高亢"; +"Ringing" = "嘹亮"; +"Spacial" = "空旷"; +"Ethereal" = "空灵"; +"Old Man" = "老男孩"; +"Baby Boy" = "小男孩"; +"Baby Girl" = "小女孩"; +"ZhuBaJie" = "猪八戒"; +"Hulk" = "绿巨人"; +"FxUncle" = "大叔"; +"FxSister" = "小姐姐"; +"Pop" = "流行"; +"Pop(Old Version)" = "流行(旧版)"; +"R&B" = "R&B"; +"R&B(Old Version)" = "R&B(旧版)"; +"Rock" = "摇滚"; +"HipHop" = "嘻哈"; +"Vocal Concert" = "演唱会"; +"Vocal Concert(Old Version)" = "演唱会(旧版)"; +"KTV" = "KTV"; +"KTV(Old Version)" = "KTV(旧版)"; +"Studio" = "录音棚"; +"Studio(Old Version)" = "录音棚(旧版)"; +"Phonograph" = "留声机"; +"Virtual Stereo" = "虚拟立体声"; +"Dry Level" = "原始声音强度"; +"Wet Level" = "早期反射信号强度"; +"Room Size" = "房间尺寸"; +"Wet Delay" = "早期反射信号延迟"; +"Strength" = "混响持续强度"; +"ARKit is not available on this device." = "当前设备不支持ARKit"; +"This app requires world tracking, which is available only on iOS devices with the A9 processor or later." = "AR功能仅在内置A9处理器后的iOS机型支持"; +"Move Camera to find a planar\n(Shown as Red Rectangle)" = "移动相机以找到一个平面\n(以红色方块显示)"; +"Tap to place remote video canvas" = "点击屏幕以放置视频画布"; +"Resolution" = "分辨率"; +"Frame Rate" = "帧率"; +"Orientation" = "视频朝向"; +"Broadcaster" = "主播"; +"Audience" = "观众"; +"Pick Role" = "选择角色"; +"Create Data Stream" = "创建数据流"; +"Raw Audio Data" = "音频裸数据"; +"Group Video Chat" = "多人音视频通话"; diff --git a/iOS/APIExample/zh-Hans.lproj/Main.strings b/iOS/APIExample/zh-Hans.lproj/Main.strings new file mode 100644 index 000000000..e2533934f --- /dev/null +++ b/iOS/APIExample/zh-Hans.lproj/Main.strings @@ -0,0 +1,29 @@ + +/* Class = "UIViewController"; title = "Agora API Examples"; ObjectID = "BYZ-38-t0r"; */ +"BYZ-38-t0r.title" = "Agora API Examples"; + +/* Class = "UILabel"; text = "0"; ObjectID = "GRE-S2-EUw"; */ +"GRE-S2-EUw.text" = "0"; + +/* Class = "UITextField"; placeholder = "Enter channel name"; ObjectID = "GWc-L5-fZV"; */ +"GWc-L5-fZV.placeholder" = "输入频道名"; + +/* Class = "UINavigationItem"; title = "Join Channel"; ObjectID = "O4p-Hd-Lr5"; */ +"O4p-Hd-Lr5.title" = "Join Channel"; + +/* Class = "UILabel"; text = "Label"; ObjectID = "Ruy-K9-CLg"; */ +"Ruy-K9-CLg.text" = "Label"; + +/* Class = "UINavigationItem"; title = "Agora API Example"; ObjectID = "Ygc-Og-WKK"; */ +"Ygc-Og-WKK.title" = "Agora API Example"; + +/* Class = "UIViewController"; title = "Log View Controller"; ObjectID = "ekP-NH-UjU"; */ +"ekP-NH-UjU.title" = "Log View Controller"; + +/* Class = "UIViewController"; title = "Join Channel Video"; ObjectID = "iib-g5-GmB"; */ +"iib-g5-GmB.title" = "Join Channel Video"; + +/* Class = "UIButton"; normalTitle = "Join"; ObjectID = "kbN-ZR-nNn"; */ +"kbN-ZR-nNn.normalTitle" = "加入频道"; + +"492-pb-Xmc.title" = "设置"; diff --git a/iOS/Agora-ScreenShare-Extension/Agora-ScreenShare-Extension-Bridging-Header.h b/iOS/Agora-ScreenShare-Extension/Agora-ScreenShare-Extension-Bridging-Header.h new file mode 100644 index 000000000..34537542d --- /dev/null +++ b/iOS/Agora-ScreenShare-Extension/Agora-ScreenShare-Extension-Bridging-Header.h @@ -0,0 +1,5 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import "AgoraAudioTube.h" diff --git a/iOS/Agora-ScreenShare-Extension/AgoraAudioTube.h b/iOS/Agora-ScreenShare-Extension/AgoraAudioTube.h new file mode 100644 index 000000000..88390ef0b --- /dev/null +++ b/iOS/Agora-ScreenShare-Extension/AgoraAudioTube.h @@ -0,0 +1,19 @@ +// +// AgoraAudioTube.h +// Agora-Screen-Sharing-iOS-Broadcast +// +// Created by CavanSu on 2019/12/4. +// Copyright © 2019 Agora. All rights reserved. +// + +#import +#import + +typedef NS_OPTIONS(NSUInteger, AudioType) { + AudioTypeApp = 1, + AudioTypeMic = 2 +}; + +@interface AgoraAudioTube : NSObject ++ (void)agoraKit:(AgoraRtcEngineKit * _Nonnull)agoraKit pushAudioCMSampleBuffer:(CMSampleBufferRef _Nonnull)sampleBuffer resampleRate:(NSUInteger)resampleRate type:(AudioType)type; +@end diff --git a/iOS/Agora-ScreenShare-Extension/AgoraAudioTube.mm b/iOS/Agora-ScreenShare-Extension/AgoraAudioTube.mm new file mode 100644 index 000000000..9393f7b3d --- /dev/null +++ b/iOS/Agora-ScreenShare-Extension/AgoraAudioTube.mm @@ -0,0 +1,377 @@ +// +// AgoraAudioTube.m +// Agora-Screen-Sharing-iOS-Broadcast +// +// Created by CavanSu on 2019/12/4. +// Copyright © 2019 Agora. All rights reserved. +// + +#import "AgoraAudioTube.h" +#import +#include "external_resampler.h" + +#pragma mark - Audio Buffer +const int bufferSize = 48000; +int16_t appAudio[bufferSize]; +int16_t micAudio[bufferSize]; +int appAudioIndex = 0; +int micAudioIndex = 0; + +#pragma mark - Resample +int resampleApp(int16_t* sourceBuffer, size_t sourceBufferSize, size_t totalSamples, int inDataSamplesPer10ms, int outDataSamplesPer10ms, int channels, int sampleRate, int resampleRate); +int resampleMic(int16_t* sourceBuffer, size_t sourceBufferSize, size_t totalSamples, int inDataSamplesPer10ms, int outDataSamplesPer10ms, int channels, int sampleRate, int resampleRate); + +static external_resampler* resamplerAppLeft; +static external_resampler* resamplerAppRight; +static external_resampler* resampleMicLeft; +static external_resampler* resampleMicRight; + +// App +int16_t inLeftAppResampleBuffer[bufferSize]; +int16_t inRightAppResampleBuffer[bufferSize]; + +int inLeftAppResampleBufferIndex = 0; +int inRightAppResampleBufferIndex = 0; + +// Mic +int16_t inLeftMicResampleBuffer[bufferSize]; +int16_t inRightMicResampleBuffer[bufferSize]; + +int inLeftMicResampleBufferIndex = 0; +int inRightMicResampleBufferIndex = 0; + +// Resample Out Buffer +int16_t outLeftResampleBuffer[bufferSize]; +int16_t outRightResampleBuffer[bufferSize]; + +int outLeftResampleBufferIndex = 0; +int outRightResampleBufferIndex = 0; + +static NSObject *lock = [[NSObject alloc] init]; + +@implementation AgoraAudioTube + ++ (void)agoraKit:(AgoraRtcEngineKit * _Nonnull)agoraKit pushAudioCMSampleBuffer:(CMSampleBufferRef _Nonnull)sampleBuffer resampleRate:(NSUInteger)resampleRate type:(AudioType)type; { + + @synchronized (lock) { + [self privateAgoraKit:agoraKit + pushAudioCMSampleBuffer:sampleBuffer + resampleRate:resampleRate + type:type]; + } +} + ++ (void)privateAgoraKit:(AgoraRtcEngineKit * _Nonnull)agoraKit pushAudioCMSampleBuffer:(CMSampleBufferRef _Nonnull)sampleBuffer resampleRate:(NSUInteger)resampleRate type:(AudioType)type { + CFRetain(sampleBuffer); + + OSStatus err = noErr; + + CMBlockBufferRef audioBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); + size_t lengthAtOffset; + size_t totalBytes; + char *samples; + err = CMBlockBufferGetDataPointer(audioBuffer, + 0, + &lengthAtOffset, + &totalBytes, + &samples); + + if (totalBytes == 0) { + CFRelease(sampleBuffer); + return; + } + + CMAudioFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer); + const AudioStreamBasicDescription *description = CMAudioFormatDescriptionGetStreamBasicDescription(format); + + size_t dataPointerSize = 0; + + if (description->mChannelsPerFrame == 1) { + dataPointerSize = bufferSize * 2; + } else { + dataPointerSize = bufferSize; + } + + char dataPointer[dataPointerSize]; + err = CMBlockBufferCopyDataBytes(audioBuffer, + 0, + totalBytes, + dataPointer); + + size_t totalSamples = totalBytes / (description->mBitsPerChannel / 8); + UInt32 channels = description->mChannelsPerFrame; + Float64 sampleRate = description->mSampleRate; + + if (description->mFormatFlags & kAudioFormatFlagIsFloat) { + float* floatData = (float*)dataPointer; + int16_t* intData = (int16_t*)dataPointer; + for (int i = 0; i < totalSamples; i++) { + float tmp = floatData[i] * 32767; + intData[i] = (tmp >= 32767) ? 32767 : tmp; + intData[i] = (tmp < -32767) ? -32767 : tmp; + } + } + + if (description->mFormatFlags & kAudioFormatFlagIsBigEndian) { + uint8_t* p = (uint8_t*)dataPointer; + for (int i = 0; i < totalBytes; i += 2) { + uint8_t tmp; + tmp = p[i]; + p[i] = p[i + 1]; + p[i + 1] = tmp; + } + } + + if ((description->mFormatFlags & kAudioFormatFlagIsNonInterleaved) && channels == 2) { + int16_t* intData = (int16_t*)dataPointer; + int16_t newBuffer[totalSamples]; + for (int i = 0; i < totalSamples / 2; i++) { + newBuffer[2 * i] = intData[i]; + newBuffer[2 * i + 1] = intData[totalSamples / 2 + i]; + } + memcpy(dataPointer, newBuffer, sizeof(int16_t) * totalSamples); + } + + // convert mono to stereo + if (channels == 1) { + int16_t* intData = (int16_t*)dataPointer; + int16_t newBuffer[totalSamples * 2]; + for (int i = 0; i < totalSamples; i++) { + newBuffer[2 * i] = intData[i]; + newBuffer[2 * i + 1] = intData[i]; + } + totalSamples *= 2; + memcpy(dataPointer, newBuffer, sizeof(int16_t) * totalSamples); + totalBytes *= 2; + channels = 2; + } + + // ResampleRate + if (sampleRate != resampleRate) { + int inDataSamplesPer10ms = sampleRate / 100; + int outDataSamplesPer10ms = (int)resampleRate / 100; + + int16_t* intData = (int16_t*)dataPointer; + + switch (type) { + case AudioTypeApp: + totalSamples = resampleApp(intData, dataPointerSize, totalSamples, + inDataSamplesPer10ms, outDataSamplesPer10ms, channels, sampleRate, (int)resampleRate); + break; + case AudioTypeMic: + totalSamples = resampleMic(intData, dataPointerSize, totalSamples, + inDataSamplesPer10ms, outDataSamplesPer10ms, channels, sampleRate, (int)resampleRate); + break; + } + + totalBytes = totalSamples * sizeof(int16_t); + } + + CMTime time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); + + switch (type) { + case AudioTypeApp: { + memcpy(appAudio + appAudioIndex, dataPointer, totalBytes); + appAudioIndex += totalSamples; + + int mixIndex = appAudioIndex > micAudioIndex ? micAudioIndex : appAudioIndex; + + if (mixIndex <= 0 || mixIndex > micAudioIndex || mixIndex > appAudioIndex) { + CFRelease(sampleBuffer); + return; + } + + int16_t pushBuffer[appAudioIndex]; + + memcpy(pushBuffer, appAudio, appAudioIndex * sizeof(int16_t)); + + for (int i = 0; i < mixIndex; i ++) { + pushBuffer[i] = (appAudio[i] + micAudio[i]) / 2; + } + + [agoraKit pushExternalAudioFrameRawData:pushBuffer + samples:appAudioIndex / 2 + timestamp:CMTimeGetSeconds(time)]; + + memset(appAudio, 0, bufferSize * sizeof(int16_t)); + appAudioIndex = 0; + + memmove(micAudio, micAudio + mixIndex, (bufferSize - mixIndex) * sizeof(int16_t)); + micAudioIndex -= mixIndex; + } + break; + case AudioTypeMic: { + memcpy(micAudio + micAudioIndex, dataPointer, totalBytes); + micAudioIndex += totalSamples; + } + break; + } + + CFRelease(sampleBuffer); +} + +int resampleApp(int16_t* sourceBuffer, size_t sourceBufferSize, size_t totalSamples, int inDataSamplesPer10ms, int outDataSamplesPer10ms, int channels, int sampleRate, int resampleRate) +{ + int16_t* intData = (int16_t*)sourceBuffer; + for (int i = 0; i < totalSamples; i ++) { + if (i % 2) { + inRightAppResampleBuffer[inRightAppResampleBufferIndex] = intData[i]; + inRightAppResampleBufferIndex ++; + } else { + inLeftAppResampleBuffer[inLeftAppResampleBufferIndex] = intData[i]; + inLeftAppResampleBufferIndex ++; + } + } + + if (!resamplerAppLeft) { + resamplerAppLeft = new external_resampler(); + } + + if (!resamplerAppRight) { + resamplerAppRight = new external_resampler(); + } + + int pPos = 0; + + // App Right + while (inRightAppResampleBufferIndex > inDataSamplesPer10ms) { + resamplerAppRight->do_resample(inRightAppResampleBuffer + pPos, + inDataSamplesPer10ms, + channels / 2, + sampleRate, + + outRightResampleBuffer + outRightResampleBufferIndex, + outDataSamplesPer10ms, + channels / 2, + (int)resampleRate); + + pPos += inDataSamplesPer10ms; + inRightAppResampleBufferIndex -= inDataSamplesPer10ms; + outRightResampleBufferIndex += outDataSamplesPer10ms; + } + + memmove(inRightAppResampleBuffer, + inRightAppResampleBuffer + pPos, + sizeof(int16_t) * (bufferSize - pPos)); + + // App Left + pPos = 0; + + while (inLeftAppResampleBufferIndex > inDataSamplesPer10ms) { + resamplerAppLeft->do_resample(inLeftAppResampleBuffer + pPos, + inDataSamplesPer10ms, + channels / 2, + sampleRate, + + outLeftResampleBuffer + outLeftResampleBufferIndex, + outDataSamplesPer10ms, + channels / 2, + (int)resampleRate); + + pPos += inDataSamplesPer10ms; + inLeftAppResampleBufferIndex -= inDataSamplesPer10ms; + outLeftResampleBufferIndex += outDataSamplesPer10ms; + } + + memmove(inLeftAppResampleBuffer, + inLeftAppResampleBuffer + pPos, + sizeof(int16_t) * (bufferSize - pPos)); + + memset(intData, 0, sourceBufferSize); + + for (int i = 0; i < outRightResampleBufferIndex; i ++) { + intData[2 * i] = outRightResampleBuffer[i]; + intData[2 * i + 1] = outLeftResampleBuffer[i]; + } + + int samples = outLeftResampleBufferIndex * 2; + // Reset + outLeftResampleBufferIndex = 0; + outRightResampleBufferIndex = 0; + + return samples; +} + +int resampleMic(int16_t* sourceBuffer, size_t sourceBufferSize, size_t totalSamples, int inDataSamplesPer10ms, int outDataSamplesPer10ms, int channels, int sampleRate, int resampleRate) +{ + int16_t* intData = (int16_t*)sourceBuffer; + for (int i = 0; i < totalSamples; i ++) { + if (i % 2) { + inRightMicResampleBuffer[inRightMicResampleBufferIndex] = intData[i]; + inRightMicResampleBufferIndex ++; + } else { + inLeftMicResampleBuffer[inLeftMicResampleBufferIndex] = intData[i]; + inLeftMicResampleBufferIndex ++; + } + } + + if (!resampleMicLeft) { + resampleMicLeft = new external_resampler(); + } + + if (!resampleMicRight) { + resampleMicRight = new external_resampler(); + } + + int pPos = 0; + + // App Right + while (inRightMicResampleBufferIndex > inDataSamplesPer10ms) { + resampleMicRight->do_resample(inRightMicResampleBuffer + pPos, + inDataSamplesPer10ms, + channels / 2, + sampleRate, + + outRightResampleBuffer + outRightResampleBufferIndex, + outDataSamplesPer10ms, + channels / 2, + (int)resampleRate); + + pPos += inDataSamplesPer10ms; + inRightMicResampleBufferIndex -= inDataSamplesPer10ms; + outRightResampleBufferIndex += outDataSamplesPer10ms; + } + + memmove(inRightMicResampleBuffer, + inRightMicResampleBuffer + pPos, + sizeof(int16_t) * (bufferSize - pPos)); + + // App Left + pPos = 0; + + while (inLeftMicResampleBufferIndex > inDataSamplesPer10ms) { + resampleMicLeft->do_resample(inLeftMicResampleBuffer + pPos, + inDataSamplesPer10ms, + channels / 2, + sampleRate, + + outLeftResampleBuffer + outLeftResampleBufferIndex, + outDataSamplesPer10ms, + channels / 2, + (int)resampleRate); + + pPos += inDataSamplesPer10ms; + inLeftMicResampleBufferIndex -= inDataSamplesPer10ms; + outLeftResampleBufferIndex += outDataSamplesPer10ms; + } + + memmove(inLeftMicResampleBuffer, + inLeftMicResampleBuffer + pPos, + sizeof(int16_t) * (bufferSize - pPos)); + + memset(intData, 0, sourceBufferSize); + + for (int i = 0; i < outRightResampleBufferIndex; i ++) { + intData[2 * i] = outRightResampleBuffer[i]; + intData[2 * i + 1] = outLeftResampleBuffer[i]; + } + + int samples = outLeftResampleBufferIndex * 2; + // Reset + outLeftResampleBufferIndex = 0; + outRightResampleBufferIndex = 0; + + return samples; +} + +@end diff --git a/iOS/Agora-ScreenShare-Extension/AgoraUploader.swift b/iOS/Agora-ScreenShare-Extension/AgoraUploader.swift new file mode 100644 index 000000000..0380f1498 --- /dev/null +++ b/iOS/Agora-ScreenShare-Extension/AgoraUploader.swift @@ -0,0 +1,108 @@ +// +// AgoraUploader.swift +// Agora-Screen-Sharing-iOS-Broadcast +// +// Created by GongYuhua on 2017/1/16. +// Copyright © 2017年 Agora. All rights reserved. +// + +import Foundation +import CoreMedia +import ReplayKit +import AgoraRtcKit + +class AgoraUploader { + private static let videoDimension : CGSize = { + let screenSize = UIScreen.main.currentMode!.size + var boundingSize = CGSize(width: 720, height: 1280) + let mW = boundingSize.width / screenSize.width + let mH = boundingSize.height / screenSize.height + if( mH < mW ) { + boundingSize.width = boundingSize.height / screenSize.height * screenSize.width + } + else if( mW < mH ) { + boundingSize.height = boundingSize.width / screenSize.width * screenSize.height + } + return boundingSize + }() + + private static let audioSampleRate: UInt = 48000 + private static let audioChannels: UInt = 2 + + private static let sharedAgoraEngine: AgoraRtcEngineKit = { + let kit = AgoraRtcEngineKit.sharedEngine(withAppId: KeyCenter.AppId, delegate: nil) + kit.setChannelProfile(.liveBroadcasting) + kit.setClientRole(.broadcaster) + + kit.enableVideo() + kit.setExternalVideoSource(true, useTexture: true, pushMode: true) + let videoConfig = AgoraVideoEncoderConfiguration(size: videoDimension, + frameRate: .fps24, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative) + kit.setVideoEncoderConfiguration(videoConfig) + kit.setAudioProfile(.musicStandardStereo, scenario: .default) + + kit.enableExternalAudioSource(withSampleRate: audioSampleRate, + channelsPerFrame: audioChannels) + + kit.muteAllRemoteVideoStreams(true) + kit.muteAllRemoteAudioStreams(true) + + return kit + }() + + static func startBroadcast(to channel: String) { + print("joining \(channel)") + sharedAgoraEngine.joinChannel(byToken: KeyCenter.Token, channelId: channel, info: nil, uid: SCREEN_SHARE_UID, joinSuccess: nil) + } + + static func sendVideoBuffer(_ sampleBuffer: CMSampleBuffer) { + guard let videoFrame = CMSampleBufferGetImageBuffer(sampleBuffer) + else { + return + } + + var rotation : Int32 = 0 + if let orientationAttachment = CMGetAttachment(sampleBuffer, key: RPVideoSampleOrientationKey as CFString, attachmentModeOut: nil) as? NSNumber { + if let orientation = CGImagePropertyOrientation(rawValue: orientationAttachment.uint32Value) { + switch orientation { + case .up, .upMirrored: rotation = 0 + case .down, .downMirrored: rotation = 180 + case .left, .leftMirrored: rotation = 90 + case .right, .rightMirrored: rotation = 270 + default: break + } + } + } + + //let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + let time = CMTime(seconds: CACurrentMediaTime(), preferredTimescale: 1000) + + let frame = AgoraVideoFrame() + frame.format = 12 + frame.time = time + frame.textureBuf = videoFrame + frame.rotation = rotation + sharedAgoraEngine.pushExternalVideoFrame(frame) + } + + static func sendAudioAppBuffer(_ sampleBuffer: CMSampleBuffer) { + AgoraAudioTube.agoraKit(sharedAgoraEngine, + pushAudioCMSampleBuffer: sampleBuffer, + resampleRate: audioSampleRate, + type: .app) + } + + static func sendAudioMicBuffer(_ sampleBuffer: CMSampleBuffer) { + AgoraAudioTube.agoraKit(sharedAgoraEngine, + pushAudioCMSampleBuffer: sampleBuffer, + resampleRate: audioSampleRate, + type: .mic) + } + + static func stopBroadcast() { + print("leaving") + sharedAgoraEngine.leaveChannel(nil) + } +} diff --git a/iOS/Agora-ScreenShare-Extension/Info.plist b/iOS/Agora-ScreenShare-Extension/Info.plist new file mode 100644 index 000000000..ae32adce5 --- /dev/null +++ b/iOS/Agora-ScreenShare-Extension/Info.plist @@ -0,0 +1,33 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Agora-ScreenShare-Extension + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSExtension + + NSExtensionPointIdentifier + com.apple.broadcast-services-upload + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).SampleHandler + RPBroadcastProcessMode + RPBroadcastProcessModeSampleBuffer + + + diff --git a/iOS/Agora-ScreenShare-Extension/SampleHandler.swift b/iOS/Agora-ScreenShare-Extension/SampleHandler.swift new file mode 100644 index 000000000..40e7eed8d --- /dev/null +++ b/iOS/Agora-ScreenShare-Extension/SampleHandler.swift @@ -0,0 +1,83 @@ +// +// SampleHandler.swift +// Agora-Screen-Sharing-iOS-Broadcast +// +// Created by GongYuhua on 2017/8/1. +// Copyright © 2017年 Agora. All rights reserved. +// + +import ReplayKit + +class SampleHandler: RPBroadcastSampleHandler { + + var bufferCopy: CMSampleBuffer? + var lastSendTs: Int64 = Int64(Date().timeIntervalSince1970 * 1000) + var timer: Timer? + + override func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) { + + if let setupInfo = setupInfo, let channel = setupInfo["channelName"] as? String { + //In-App Screen Capture + AgoraUploader.startBroadcast(to: channel) + } else { + // iOS Screen Record and Broadcast + // IMPORTANT + // You have to use App Group to pass information/parameter + // from main app to extension + // in this demo we don't introduce app group as it increases complexity + // this is the reason why channel name is hardcoded to be ScreenShare + // You may use a dynamic channel name through keychain or userdefaults + // after enable app group feature + AgoraUploader.startBroadcast(to: "ScreenShare") + } + DispatchQueue.main.async { + self.timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) {[weak self] (timer:Timer) in + guard let weakSelf = self else {return} + let elapse = Int64(Date().timeIntervalSince1970 * 1000) - weakSelf.lastSendTs + print("elapse: \(elapse)") + // if frame stopped sending for too long time, resend the last frame + // to avoid stream being frozen when viewed from remote + if(elapse > 300) { + if let buffer = weakSelf.bufferCopy { + weakSelf.processSampleBuffer(buffer, with: .video) + } + } + } + } + } + + override func broadcastPaused() { + // User has requested to pause the broadcast. Samples will stop being delivered. + } + + override func broadcastResumed() { + // User has requested to resume the broadcast. Samples delivery will resume. + } + + override func broadcastFinished() { + timer?.invalidate() + timer = nil + AgoraUploader.stopBroadcast() + } + + override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) { + DispatchQueue.main.async {[weak self] in + switch sampleBufferType { + case .video: + if let weakSelf = self { + weakSelf.bufferCopy = sampleBuffer + weakSelf.lastSendTs = Int64(Date().timeIntervalSince1970 * 1000) + } + AgoraUploader.sendVideoBuffer(sampleBuffer) + case .audioApp: + AgoraUploader.sendAudioAppBuffer(sampleBuffer) + break + case .audioMic: + AgoraUploader.sendAudioMicBuffer(sampleBuffer) + break + @unknown default: + break + } + } + } +} diff --git a/iOS/Agora-ScreenShare-Extension/external_resampler.h b/iOS/Agora-ScreenShare-Extension/external_resampler.h new file mode 100755 index 000000000..7511bd1d4 --- /dev/null +++ b/iOS/Agora-ScreenShare-Extension/external_resampler.h @@ -0,0 +1,23 @@ +#ifndef AGORA_AUDIO_EXTERNAL_RESAMPLER_H_ +#define AGORA_AUDIO_EXTERNAL_RESAMPLER_H_ + +class external_resampler { + +public: + external_resampler(); + ~external_resampler(); + + int do_resample(short* in, + int in_samples, + int in_channels, + int in_samplerate, + short* out, + int out_samples, + int out_channels, + int out_samplerate); + +private: + void* resampler = nullptr; +}; + +#endif diff --git a/iOS/Agora-ScreenShare-Extension/libios_resampler.a b/iOS/Agora-ScreenShare-Extension/libios_resampler.a new file mode 100644 index 000000000..0a6a7088b Binary files /dev/null and b/iOS/Agora-ScreenShare-Extension/libios_resampler.a differ diff --git a/iOS/Podfile b/iOS/Podfile index e75786d8c..90351fdab 100644 --- a/iOS/Podfile +++ b/iOS/Podfile @@ -1,11 +1,17 @@ # Uncomment the next line to define a global platform for your project -# platform :ios, '9.0' +platform :ios, '11.0' target 'APIExample' do - # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + pod 'Floaty', '~> 4.2.0' + pod 'AGEVideoLayout', '~> 1.0.2' + pod 'AgoraRtcEngine_iOS', '3.3.0' + pod 'AgoraMediaPlayer_iOS', '1.2.2' +end + +target 'Agora-ScreenShare-Extension' do - # Pods for APIExample - pod 'AGEVideoLayout' - pod 'AgoraRtcEngine_iOS', '3.0.0' + use_frameworks! + pod 'AgoraRtcEngine_iOS', '3.3.0' end diff --git a/iOS/README.md b/iOS/README.md new file mode 100644 index 000000000..cc64ecf6f --- /dev/null +++ b/iOS/README.md @@ -0,0 +1,68 @@ +# API Example iOS + +*English | [中文](README.zh.md)* + +This project presents you a set of API examples to help you understand how to use Agora APIs. + +## Problem +After users upgrade their iOS devices to iOS 14.0, and use an app that integrates the Agora RTC SDK for iOS for the first time, users see a prompt for finding local network devices. The following picture shows the pop-up prompt: + +![](./pictures/ios_14_privacy.png) + +[Solution](https://docs.agora.io/en/faq/local_network_privacy) + +## Prerequisites + +- Xcode 10.0+ +- Physical iOS device (iPhone or iPad) +- iOS simulator is NOT supported + +## Quick Start + +This section shows you how to prepare, build, and run the sample application. + +### Prepare Dependencies + +Change directory into **iOS** folder, run following command to install project dependencies, + +``` +pod install +``` + +Verify `APIExample.xcworkspace` has been properly generated. + +### Obtain an App Id + +To build and run the sample application, get an App Id: + +1. Create a developer account at [agora.io](https://dashboard.agora.io/signin/). Once you finish the signup process, you will be redirected to the Dashboard. +2. Navigate in the Dashboard tree on the left to **Projects** > **Project List**. +3. Save the **App Id** from the Dashboard for later use. +4. Generate a temp **Access Token** (valid for 24 hours) from dashboard page with given channel name, save for later use. + +5. Open `APIExample.xcworkspace` and edit the `KeyCenter.swift` file. In the `KeyCenter` struct, update `<#Your App Id#>` with your App Id, and change `<#Temp Access Token#>` with the temp Access Token generated from dashboard. Note you can leave the token variable `nil` if your project has not turned on security token. + + ``` Swift + struct KeyCenter { + static let AppId: String = <#Your App Id#> + + // assign token to nil if you have not enabled app certificate + static var Token: String? = <#Temp Access Token#> + } + ``` + +You are all set. Now connect your iPhone or iPad device and run the project. + +## Contact Us + +- For potential issues, take a look at our [FAQ](https://docs.agora.io/en/faq) first +- Dive into [Agora SDK Samples](https://github.com/AgoraIO) to see more tutorials +- Take a look at [Agora Use Case](https://github.com/AgoraIO-usecase) for more complicated real use case +- Repositories managed by developer communities can be found at [Agora Community](https://github.com/AgoraIO-Community) +- You can find full API documentation at [Document Center](https://docs.agora.io/en/) +- If you encounter problems during integration, you can ask question in [Stack Overflow](https://stackoverflow.com/questions/tagged/agora.io) +- You can file bugs about this sample at [issue](https://github.com/AgoraIO/Basic-Video-Call/issues) + +## License + +The MIT License (MIT) diff --git a/iOS/README.zh.md b/iOS/README.zh.md new file mode 100644 index 000000000..2f3e224b6 --- /dev/null +++ b/iOS/README.zh.md @@ -0,0 +1,66 @@ +# API Example iOS + +*[English](README.md) | 中文* + +这个开源示例项目演示了Agora视频SDK的部分API使用示例,以帮助开发者更好地理解和运用Agora视频SDK的API。 + +## 问题描述 +iOS 系统版本升级至 14.0 版本后,用户首次使用集成了声网 iOS 语音或视频 SDK 的 app 时会看到查找本地网络设备的弹窗提示。默认弹窗界面如下图所示: + +![](./pictures/ios_14_privacy_zh.png) + +[解决方案](https://docs.agora.io/cn/faq/local_network_privacy) + +## 环境准备 + +- XCode 10.0 + +- iOS 真机设备 +- 不支持模拟器 + +## 运行示例程序 + +这个段落主要讲解了如何编译和运行实例程序。 + +### 安装依赖库 + +切换到 **iOS** 目录,运行以下命令使用CocoaPods安装依赖,Agora视频SDK会在安装后自动完成集成。 + +``` +pod install +``` + +运行后确认 `APIExample.xcworkspace` 正常生成即可。 + +### 创建Agora账号并获取AppId + +在编译和启动实例程序前,你需要首先获取一个可用的App Id: + +1. 在[agora.io](https://dashboard.agora.io/signin/)创建一个开发者账号 +2. 前往后台页面,点击左部导航栏的 **项目 > 项目列表** 菜单 +3. 复制后台的 **App Id** 并备注,稍后启动应用时会用到它 +4. 在项目页面生成临时 **Access Token** (24小时内有效)并备注,注意生成的Token只能适用于对应的频道名。 + +5. 打开 `APIExample.xcworkspace` 并编辑 `KeyCenter.swift`,将你的 AppID 和 Token 分别替换到 `<#Your App Id#>` 与 `<#Temp Access Token#>` + + ``` + let AppID: String = <#Your App Id#> + // 如果你没有打开Token功能,token可以直接给nil + let Token: String? = <#Temp Access Token#> + ``` + +然后你就可以使用 `APIExample.xcworkspace` 编译并运行项目了。 + +## 联系我们 + +- 如果你遇到了困难,可以先参阅 [常见问题](https://docs.agora.io/cn/faq) +- 如果你想了解更多官方示例,可以参考 [官方SDK示例](https://github.com/AgoraIO) +- 如果你想了解声网SDK在复杂场景下的应用,可以参考 [官方场景案例](https://github.com/AgoraIO-usecase) +- 如果你想了解声网的一些社区开发者维护的项目,可以查看 [社区](https://github.com/AgoraIO-Community) +- 完整的 API 文档见 [文档中心](https://docs.agora.io/cn/) +- 若遇到问题需要开发者帮助,你可以到 [开发者社区](https://rtcdeveloper.com/) 提问 +- 如果需要售后技术支持, 你可以在 [Agora Dashboard](https://dashboard.agora.io) 提交工单 +- 如果发现了示例代码的 bug,欢迎提交 [issue](https://github.com/AgoraIO/Basic-Video-Call/issues) + +## 代码许可 + +The MIT License (MIT) diff --git a/iOS/cicd/build-template/build-ios.yml b/iOS/cicd/build-template/build-ios.yml new file mode 100644 index 000000000..1f756aa27 --- /dev/null +++ b/iOS/cicd/build-template/build-ios.yml @@ -0,0 +1,37 @@ +parameters: + displayName: '' + workingDirectory: '' + project: '' + scheme: '' + +jobs: + - job: ${{ parameters.displayName }}Build + displayName: ${{ parameters.displayName }} + + pool: + vmImage: 'macOS-10.14' + + variables: + - group: AgoraKeys + + steps: + - script: cd '${{parameters.workingDirectory}}/cicd/scripts' && ls && python keycenter.py && ls + env: + AGORA_APP_ID: $(agora.appId) + File_Directory: '../../${{ parameters.project }}/Common' + + - task: InstallAppleCertificate@2 + inputs: + certSecureFile: 'certificate.p12' + certPwd: $(agora.password) + + - task: InstallAppleProvisioningProfile@1 + inputs: + provProfileSecureFile: 'AgoraAppsDevProfile.mobileprovision' + + - script: cd '${{parameters.workingDirectory}}/cicd/scripts' && chmod +x ios_build.sh && ./ios_build.sh ../../ ${{ parameters.project }} ${{ parameters.scheme }} + + - task: PublishBuildArtifacts@1 + inputs: + PathtoPublish: ${{ parameters.workingDirectory }}/app + ArtifactName: ${{ parameters.displayName }} diff --git a/iOS/cicd/build-template/build-mac.yml b/iOS/cicd/build-template/build-mac.yml new file mode 100644 index 000000000..6c609c45c --- /dev/null +++ b/iOS/cicd/build-template/build-mac.yml @@ -0,0 +1,41 @@ +parameters: + displayName: '' + workingDirectory: '' + scheme: '' + sdkurl: '' + bundleid: '' + username: '' + password: '' + ascprovider: '' + +jobs: + - job: ${{ parameters.displayName }}Build + displayName: ${{ parameters.displayName }} + + pool: + vmImage: 'macOS-10.14' + + variables: + - group: AgoraKeys + + steps: + - script: cd '${{parameters.workingDirectory}}/cicd/scripts' && ls && python keycenter.py && ls + env: + AGORA_APP_ID: $(agora.appId) + File_Directory: '../../${{ parameters.workingDirectory }}/${{ parameters.project }}/Commons' + + - task: InstallAppleCertificate@2 + inputs: + certSecureFile: 'apiexamplemac.p12' + certPwd: $(agora.api.example.mac.cert.pass) + + - task: InstallAppleProvisioningProfile@1 + inputs: + provProfileSecureFile: 'apiexamplemac.provisionprofile' + + - script: cd '${{parameters.workingDirectory}}/cicd/scripts' && chmod +x mac_build.sh && ./mac_build.sh ../../${{ parameters.workingDirectory }} ${{ parameters.project }} ${{ parameters.scheme }} ${{parameters.bundleid}} ${{parameters.username}} $(agora.api.example.mac.notarize.pass) ${{parameters.ascprovider}} + + - task: PublishBuildArtifacts@1 + inputs: + PathtoPublish: ${{ parameters.workingDirectory }}/${{ parameters.scheme }}.zip + ArtifactName: ${{ parameters.displayName }} \ No newline at end of file diff --git a/iOS/cicd/build-template/github-release.yml b/iOS/cicd/build-template/github-release.yml new file mode 100644 index 000000000..46e5c7aa9 --- /dev/null +++ b/iOS/cicd/build-template/github-release.yml @@ -0,0 +1,3 @@ +parameters: + displayName: '' + diff --git a/iOS/cicd/scripts/ios_build.sh b/iOS/cicd/scripts/ios_build.sh new file mode 100755 index 000000000..6ba2a543f --- /dev/null +++ b/iOS/cicd/scripts/ios_build.sh @@ -0,0 +1,40 @@ +WORKING_PATH=$1 +APP_Project=$2 +APP_TARGET=$3 +MODE=Release + +echo "WORKING_PATH: ${WORKING_PATH}" +echo "APP_TARGET: ${APP_TARGET}" + +cd ${WORKING_PATH} +echo `pwd` + +rm -f *.ipa +rm -rf *.app +rm -f *.zip +rm -rf dSYMs +rm -rf *.dSYM +rm -f *dSYMs.zip +rm -rf *.xcarchive + +Export_Plist_File=exportPlist.plist + +BUILD_DATE=`date +%Y-%m-%d-%H.%M.%S` +ArchivePath=${APP_TARGET}-${BUILD_DATE}.xcarchive + +TARGET_FILE="" +if [ ! -f "Podfile" ];then +TARGET_FILE="${APP_Project}.xcodeproj" +xcodebuild clean -project ${TARGET_FILE} -scheme "${APP_TARGET}" -configuration ${MODE} +xcodebuild -project ${TARGET_FILE} -scheme "${APP_TARGET}" -configuration ${MODE} -archivePath ${ArchivePath} archive +else +pod install +TARGET_FILE="${APP_Project}.xcworkspace" +xcodebuild clean -workspace ${TARGET_FILE} -scheme "${APP_TARGET}" -configuration ${MODE} +xcodebuild -workspace ${TARGET_FILE} -scheme "${APP_TARGET}" -configuration ${MODE} -archivePath ${ArchivePath} archive +fi + +xcodebuild -exportArchive -exportOptionsPlist ${Export_Plist_File} -archivePath ${ArchivePath} -exportPath . + +mkdir app +mv *.ipa app && mv *.xcarchive app diff --git a/iOS/cicd/scripts/keycenter.py b/iOS/cicd/scripts/keycenter.py new file mode 100644 index 000000000..f900ddaf1 --- /dev/null +++ b/iOS/cicd/scripts/keycenter.py @@ -0,0 +1,50 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- +import re +import os + +def main(): + appId = "" + if "AGORA_APP_ID" in os.environ: + appId = os.environ["AGORA_APP_ID"] + token = "" + + fileDirectory = "" + if "File_Directory" in os.environ: + fileDirectory = os.environ["File_Directory"] + + # KeyCenter.swift + KeyCenterPath = fileDirectory + "/KeyCenter.swift" + print("KeyCenterPath: %s" %KeyCenterPath) + + try: + f = open(KeyCenterPath, 'r+') + content = f.read() + appString = "\"" + appId + "\"" + tokenString = "\"" + token + "\"" + contentNew = re.sub(r'<#Your App Id#>', appString, content) + contentNew = re.sub(r'<#Temp Access Token#>', tokenString, contentNew) + f.seek(0) + f.write(contentNew) + f.truncate() + except IOError: + print("Swift File is not accessible.") + + # KeyCenter.m + KeyCenterPath = fileDirectory + "/KeyCenter.m" + + try: + f = open(KeyCenterPath, 'r+') + content = f.read() + appString = "@\"" + appId + "\"" + tokenString = "@\"" + token + "\"" + contentNew = re.sub(r'<#Your App Id#>', appString, content) + contentNew = re.sub(r'<#Temp Access Token#>', tokenString, contentNew) + f.seek(0) + f.write(contentNew) + f.truncate() + except IOError: + print("OC File is not accessible.") + +if __name__ == "__main__": + main() diff --git a/iOS/cicd/scripts/mac_build.sh b/iOS/cicd/scripts/mac_build.sh new file mode 100755 index 000000000..2571933d7 --- /dev/null +++ b/iOS/cicd/scripts/mac_build.sh @@ -0,0 +1,47 @@ +WORKING_PATH=$1 +APP_Project=$2 +APP_TARGET=$3 +BUNDLE_ID=$4 +USERNAME=$5 +PASSWORD=$6 +ASCPROVIDER=$7 +MODE=Release + +echo "WORKING_PATH: ${WORKING_PATH}" +echo "APP_TARGET: ${APP_TARGET}" +echo "PROVIDER: ${ASCPROVIDER}" + +cd ${WORKING_PATH} +echo `pwd` + +rm -f *.ipa +rm -rf *.app +rm -f *.zip +rm -rf dSYMs +rm -rf *.dSYM +rm -f *dSYMs.zip +rm -rf *.xcarchive + +Export_Plist_File=exportPlist.plist + +BUILD_DATE=`date +%Y-%m-%d-%H.%M.%S` +ArchivePath=${APP_TARGET}-${BUILD_DATE}.xcarchive + +TARGET_FILE="" +if [ ! -f "Podfile" ];then +TARGET_FILE="${APP_Project}.xcodeproj" +xcodebuild clean -project ${TARGET_FILE} -scheme "${APP_TARGET}" -configuration ${MODE} +xcodebuild -project ${TARGET_FILE} -scheme "${APP_TARGET}" -configuration ${MODE} -archivePath ${ArchivePath} archive +else +pod install +TARGET_FILE="${APP_Project}.xcworkspace" +xcodebuild clean -workspace ${TARGET_FILE} -scheme "${APP_TARGET}" -configuration ${MODE} +xcodebuild -workspace ${TARGET_FILE} -scheme "${APP_TARGET}" -configuration ${MODE} -archivePath ${ArchivePath} archive +fi + +xcodebuild -exportArchive -exportOptionsPlist ${Export_Plist_File} -archivePath ${ArchivePath} -exportPath . + +ls -alt + +ditto -c -k --keepParent ${APP_TARGET}.app ${APP_TARGET}.zip +xcrun altool --notarize-app -f ${APP_TARGET}.zip --primary-bundle-id ${BUNDLE_ID} --asc-provider ${ASCPROVIDER} --username ${USERNAME} --password ${PASSWORD} \ No newline at end of file diff --git a/iOS/clear.sh b/iOS/clear.sh new file mode 100755 index 000000000..18d57fc51 --- /dev/null +++ b/iOS/clear.sh @@ -0,0 +1,11 @@ +rm -rf *.xcarchive +rm -f *.ipa +rm -rf *.app +rm -f DistributionSummary.plist +rm -f ExportOptions.plist +rm -f Packaging.log +rm -rf app +rm -f app.zip +# rm -f Podfile.lock +# rm -rf Pods +# rm -rf *.xcworkspace \ No newline at end of file diff --git a/iOS/exportPlist.plist b/iOS/exportPlist.plist new file mode 100644 index 000000000..aee431a33 --- /dev/null +++ b/iOS/exportPlist.plist @@ -0,0 +1,17 @@ + + + + + method + development + compileBitcode + + provisioningProfiles + + io.agora.api.example + App + io.agora.api.example.Agora-ScreenShare-Extension + App + + + diff --git a/iOS/pictures/ios_14_privacy.png b/iOS/pictures/ios_14_privacy.png new file mode 100644 index 000000000..0d171d537 Binary files /dev/null and b/iOS/pictures/ios_14_privacy.png differ diff --git a/iOS/pictures/ios_14_privacy_zh.png b/iOS/pictures/ios_14_privacy_zh.png new file mode 100644 index 000000000..d225f3ea8 Binary files /dev/null and b/iOS/pictures/ios_14_privacy_zh.png differ diff --git a/macOS/.gitignore b/macOS/.gitignore new file mode 100644 index 000000000..579929264 --- /dev/null +++ b/macOS/.gitignore @@ -0,0 +1,109 @@ +.vscode +*.xcuserdata +*.DS_Store +*.xcscmblueprint +*.framework +*.a +*.xcworkspacedata +xcshareddata + +*.zip +agora_sdk +*.xcarchive + +DistributionSummary.plist +ExportOptions.plist +Packaging.log +*.app + +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control + +Pods/ +Podfile.lock + +# Add this line if you want to avoid checking in source code from the Xcode workspace +*.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ diff --git a/macOS/APIExample.xcodeproj/project.pbxproj b/macOS/APIExample.xcodeproj/project.pbxproj new file mode 100644 index 000000000..73bb8e049 --- /dev/null +++ b/macOS/APIExample.xcodeproj/project.pbxproj @@ -0,0 +1,1481 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXBuildFile section */ + 0301D3182507B4A800DF3BEA /* AgoraMetalShader.metal in Sources */ = {isa = PBXBuildFile; fileRef = 0301D3162507B4A800DF3BEA /* AgoraMetalShader.metal */; }; + 0301D3192507B4A800DF3BEA /* AgoraMetalRender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0301D3172507B4A800DF3BEA /* AgoraMetalRender.swift */; }; + 0301D31D2507C0F300DF3BEA /* MetalVideoView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0301D31C2507C0F300DF3BEA /* MetalVideoView.xib */; }; + 03267E1C24FF3AF4004A91A6 /* AgoraCameraSourcePush.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03267E1B24FF3AF4004A91A6 /* AgoraCameraSourcePush.swift */; }; + 03267E222500C265004A91A6 /* AgoraMediaDataPlugin.mm in Sources */ = {isa = PBXBuildFile; fileRef = 03267E1F2500C265004A91A6 /* AgoraMediaDataPlugin.mm */; }; + 03267E232500C265004A91A6 /* AgoraMediaRawData.m in Sources */ = {isa = PBXBuildFile; fileRef = 03267E202500C265004A91A6 /* AgoraMediaRawData.m */; }; + 0333E63524FA30310063C5B0 /* BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0333E63424FA30310063C5B0 /* BaseViewController.swift */; }; + 0333E63724FA32000063C5B0 /* VideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0333E63624FA32000063C5B0 /* VideoView.swift */; }; + 0336A1C725034F4700D61B7F /* AudioWriteToFile.m in Sources */ = {isa = PBXBuildFile; fileRef = 0336A1BD25034F4600D61B7F /* AudioWriteToFile.m */; }; + 0336A1CA25034F4700D61B7F /* ExternalAudio.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0336A1C225034F4700D61B7F /* ExternalAudio.mm */; }; + 0336A1CB25034F4700D61B7F /* AudioController.m in Sources */ = {isa = PBXBuildFile; fileRef = 0336A1C425034F4700D61B7F /* AudioController.m */; }; + 033A9EDB252C17F200BC26E1 /* CustomVideoSourceMediaIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033A9EDA252C17F200BC26E1 /* CustomVideoSourceMediaIO.swift */; }; + 033A9EE2252C191000BC26E1 /* PrecallTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033A9EE0252C191000BC26E1 /* PrecallTest.swift */; }; + 033A9F9E252EA86A00BC26E1 /* CustomVideoSourcePush.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033A9F90252EA86A00BC26E1 /* CustomVideoSourcePush.swift */; }; + 033A9F9F252EA86A00BC26E1 /* CustomVideoRender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033A9F92252EA86A00BC26E1 /* CustomVideoRender.swift */; }; + 033A9FA0252EA86A00BC26E1 /* CustomAudioSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033A9F94252EA86A00BC26E1 /* CustomAudioSource.swift */; }; + 033A9FA1252EA86A00BC26E1 /* CustomAudioRender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033A9F96252EA86A00BC26E1 /* CustomAudioRender.swift */; }; + 033A9FA4252EA86A00BC26E1 /* RTMPStreaming.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033A9F9B252EA86A00BC26E1 /* RTMPStreaming.swift */; }; + 033A9FA5252EA86A00BC26E1 /* RawMediaData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033A9F9D252EA86A00BC26E1 /* RawMediaData.swift */; }; + 033A9FB3252EAEB500BC26E1 /* JoinChannelVideo.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033A9FB5252EAEB500BC26E1 /* JoinChannelVideo.storyboard */; }; + 033A9FB8252EAEF700BC26E1 /* JoinChannelAudio.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033A9FBA252EAEF700BC26E1 /* JoinChannelAudio.storyboard */; }; + 033A9FBD252EB02600BC26E1 /* CustomAudioRender.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033A9FBF252EB02600BC26E1 /* CustomAudioRender.storyboard */; }; + 033A9FC2252EB02D00BC26E1 /* CustomAudioSource.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033A9FC4252EB02D00BC26E1 /* CustomAudioSource.storyboard */; }; + 033A9FC7252EB03700BC26E1 /* CustomVideoRender.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033A9FC9252EB03700BC26E1 /* CustomVideoRender.storyboard */; }; + 033A9FCC252EB03F00BC26E1 /* CustomVideoSourcePush.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033A9FCE252EB03F00BC26E1 /* CustomVideoSourcePush.storyboard */; }; + 033A9FD1252EB04700BC26E1 /* RawMediaData.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033A9FD3252EB04700BC26E1 /* RawMediaData.storyboard */; }; + 033A9FD6252EB05200BC26E1 /* RTMPStreaming.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033A9FD8252EB05200BC26E1 /* RTMPStreaming.storyboard */; }; + 033A9FDB252EB05A00BC26E1 /* PrecallTest.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033A9FDD252EB05A00BC26E1 /* PrecallTest.storyboard */; }; + 033A9FE0252EB58600BC26E1 /* CustomVideoSourceMediaIO.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033A9FE2252EB58600BC26E1 /* CustomVideoSourceMediaIO.storyboard */; }; + 033A9FE5252EB59000BC26E1 /* VoiceChanger.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033A9FE7252EB59000BC26E1 /* VoiceChanger.storyboard */; }; + 033A9FE8252EB59700BC26E1 /* VoiceChanger.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033A9FE7252EB59000BC26E1 /* VoiceChanger.storyboard */; }; + 033A9FEB252EB5CC00BC26E1 /* AudioMixing.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033A9FED252EB5CC00BC26E1 /* AudioMixing.storyboard */; }; + 033A9FF0252EB5EB00BC26E1 /* ChannelMediaRelay.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033A9FF2252EB5EB00BC26E1 /* ChannelMediaRelay.storyboard */; }; + 033A9FF5252EB5F400BC26E1 /* JoinMultiChannel.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033A9FF7252EB5F400BC26E1 /* JoinMultiChannel.storyboard */; }; + 033A9FFA252EB5FD00BC26E1 /* ScreenShare.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033A9FFC252EB5FD00BC26E1 /* ScreenShare.storyboard */; }; + 033A9FFF252EB60800BC26E1 /* StreamEncryption.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033AA001252EB60800BC26E1 /* StreamEncryption.storyboard */; }; + 033AA005252EBBEC00BC26E1 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 033AA004252EBBEC00BC26E1 /* Localizable.strings */; }; + 034C626425257EA600296ECF /* GlobalSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034C626325257EA600296ECF /* GlobalSettings.swift */; }; + 034C62672525857200296ECF /* JoinChannelAudio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034C62662525857200296ECF /* JoinChannelAudio.swift */; }; + 034C626C25259FC200296ECF /* JoinChannelVideo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034C626B25259FC200296ECF /* JoinChannelVideo.swift */; }; + 034C62712525A35800296ECF /* StreamEncryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034C62702525A35700296ECF /* StreamEncryption.swift */; }; + 034C62772525C68D00296ECF /* AgoraCustomEncryption.mm in Sources */ = {isa = PBXBuildFile; fileRef = 034C62752525C68C00296ECF /* AgoraCustomEncryption.mm */; }; + 034C627C2526C43900296ECF /* ScreenShare.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034C627A2526C43900296ECF /* ScreenShare.swift */; }; + 034C62872528255F00296ECF /* WindowsCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034C62862528255F00296ECF /* WindowsCenter.swift */; }; + 034C628A25282D5D00296ECF /* JoinMultiChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034C628925282D5D00296ECF /* JoinMultiChannel.swift */; }; + 034C62912528327800296ECF /* ChannelMediaRelay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034C628F2528327800296ECF /* ChannelMediaRelay.swift */; }; + 034C62932528474D00296ECF /* StatisticsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034C62922528474D00296ECF /* StatisticsInfo.swift */; }; + 034C629C25295F2800296ECF /* AudioMixing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034C629A25295F2800296ECF /* AudioMixing.swift */; }; + 034C62A025297ABB00296ECF /* audioeffect.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 034C629E25297ABB00296ECF /* audioeffect.mp3 */; }; + 034C62A125297ABB00296ECF /* audiomixing.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 034C629F25297ABB00296ECF /* audiomixing.mp3 */; }; + 034C62A6252ABA5C00296ECF /* VoiceChanger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 034C62A4252ABA5C00296ECF /* VoiceChanger.swift */; }; + 036D3A9A24FA395E00B1D8DC /* KeyCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036D3A9924FA395E00B1D8DC /* KeyCenter.swift */; }; + 036D3A9E24FA3A1000B1D8DC /* LogUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036D3A9D24FA3A1000B1D8DC /* LogUtils.swift */; }; + 036D3AA024FA40EB00B1D8DC /* VideoView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 036D3A9F24FA40EB00B1D8DC /* VideoView.xib */; }; + 036D3AA224FAA00A00B1D8DC /* Configs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036D3AA124FAA00A00B1D8DC /* Configs.swift */; }; + 03896D3024F8A00F008593CD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03896D2F24F8A00F008593CD /* AppDelegate.swift */; }; + 03896D3224F8A00F008593CD /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03896D3124F8A00F008593CD /* ViewController.swift */; }; + 03896D3424F8A011008593CD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 03896D3324F8A011008593CD /* Assets.xcassets */; }; + 03896D3724F8A011008593CD /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 03896D3524F8A011008593CD /* Main.storyboard */; }; + 03896D4324F8A011008593CD /* APIExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03896D4224F8A011008593CD /* APIExampleTests.swift */; }; + 03896D4E24F8A011008593CD /* APIExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03896D4D24F8A011008593CD /* APIExampleUITests.swift */; }; + 03B12DA4250E8F7F00E55818 /* AgoraExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B12DA3250E8F7F00E55818 /* AgoraExtension.swift */; }; + 03B321DB24FC0D5E008EBD2C /* AgoraCameraSourceMediaIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B321D724FC0D5D008EBD2C /* AgoraCameraSourceMediaIO.swift */; }; + 4667B9F286200B077FFDFDE1 /* Pods_APIExampleTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0CA9B97F4DF8A31A030414B3 /* Pods_APIExampleTests.framework */; }; + 57645A03259B1C22007B1E30 /* CreateDataStream.strings in Resources */ = {isa = PBXBuildFile; fileRef = 576459FE259B1C22007B1E30 /* CreateDataStream.strings */; }; + 57645A04259B1C22007B1E30 /* CreateDataStream.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 57645A00259B1C22007B1E30 /* CreateDataStream.storyboard */; }; + 57645A05259B1C22007B1E30 /* CreateDataStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57645A02259B1C22007B1E30 /* CreateDataStream.swift */; }; + 5770E2D5258C9E6F00812A80 /* Picker.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5770E2D3258C9E6F00812A80 /* Picker.xib */; }; + 5770E2DF258CDCA600812A80 /* Picker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5770E2DE258CDCA600812A80 /* Picker.swift */; }; + 57887A67258856B7006E962A /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 57887A69258856B7006E962A /* Settings.storyboard */; }; + 57887A75258859D8006E962A /* SettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57887A74258859D8006E962A /* SettingsController.swift */; }; + 57887A83258886E1006E962A /* SettingCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57887A82258886E1006E962A /* SettingCells.swift */; }; + 57887A87258889ED006E962A /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57887A86258889ED006E962A /* SettingsViewController.swift */; }; + 57A635B525906D0500EDC2F7 /* Input.xib in Sources */ = {isa = PBXBuildFile; fileRef = 57A635B425906D0500EDC2F7 /* Input.xib */; }; + 57A635BB25906D5500EDC2F7 /* Input.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57A635BA25906D5500EDC2F7 /* Input.swift */; }; + 57A635D82591BC0C00EDC2F7 /* Slider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57A635D72591BC0C00EDC2F7 /* Slider.swift */; }; + 57A635DC2591BCF000EDC2F7 /* Slider.xib in Resources */ = {isa = PBXBuildFile; fileRef = 57A635DB2591BCF000EDC2F7 /* Slider.xib */; }; + 57A635F42593544600EDC2F7 /* effectA.wav in Resources */ = {isa = PBXBuildFile; fileRef = 57A635F32593544600EDC2F7 /* effectA.wav */; }; + 57AF397B259B31AA00601E02 /* RawAudioData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57AF397A259B31AA00601E02 /* RawAudioData.swift */; }; + 57AF3981259B329B00601E02 /* RawAudioData.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 57AF3980259B329B00601E02 /* RawAudioData.storyboard */; }; + 596A9F79AF0CD8DC1CA93253 /* Pods_APIExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F65EF2B97B89DE4581B426B /* Pods_APIExample.framework */; }; + EBDD0209B272C276B21B6270 /* Pods_APIExample_APIExampleUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC2BAB0AC82140B7CEEA31DA /* Pods_APIExample_APIExampleUITests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 03896D3F24F8A011008593CD /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 03896D2424F8A00F008593CD /* Project object */; + proxyType = 1; + remoteGlobalIDString = 03896D2B24F8A00F008593CD; + remoteInfo = APIExample; + }; + 03896D4A24F8A011008593CD /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 03896D2424F8A00F008593CD /* Project object */; + proxyType = 1; + remoteGlobalIDString = 03896D2B24F8A00F008593CD; + remoteInfo = APIExample; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 032C0FA2254873AC00D80A57 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0301D3162507B4A800DF3BEA /* AgoraMetalShader.metal */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.metal; path = AgoraMetalShader.metal; sourceTree = ""; }; + 0301D3172507B4A800DF3BEA /* AgoraMetalRender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AgoraMetalRender.swift; sourceTree = ""; }; + 0301D31C2507C0F300DF3BEA /* MetalVideoView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MetalVideoView.xib; sourceTree = ""; }; + 03267E1B24FF3AF4004A91A6 /* AgoraCameraSourcePush.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AgoraCameraSourcePush.swift; sourceTree = ""; }; + 03267E1E2500C265004A91A6 /* AgoraMediaRawData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AgoraMediaRawData.h; sourceTree = ""; }; + 03267E1F2500C265004A91A6 /* AgoraMediaDataPlugin.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AgoraMediaDataPlugin.mm; sourceTree = ""; }; + 03267E202500C265004A91A6 /* AgoraMediaRawData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AgoraMediaRawData.m; sourceTree = ""; }; + 03267E212500C265004A91A6 /* AgoraMediaDataPlugin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AgoraMediaDataPlugin.h; sourceTree = ""; }; + 03267E262500C779004A91A6 /* APIExample-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "APIExample-Bridging-Header.h"; sourceTree = ""; }; + 0333E63424FA30310063C5B0 /* BaseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseViewController.swift; sourceTree = ""; }; + 0333E63624FA32000063C5B0 /* VideoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoView.swift; sourceTree = ""; }; + 0336A1BC25034F4600D61B7F /* AudioOptions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AudioOptions.h; sourceTree = ""; }; + 0336A1BD25034F4600D61B7F /* AudioWriteToFile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AudioWriteToFile.m; sourceTree = ""; }; + 0336A1BE25034F4600D61B7F /* ExternalAudio.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ExternalAudio.h; sourceTree = ""; }; + 0336A1BF25034F4700D61B7F /* AudioController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AudioController.h; sourceTree = ""; }; + 0336A1C225034F4700D61B7F /* ExternalAudio.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ExternalAudio.mm; sourceTree = ""; }; + 0336A1C325034F4700D61B7F /* AudioWriteToFile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AudioWriteToFile.h; sourceTree = ""; }; + 0336A1C425034F4700D61B7F /* AudioController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AudioController.m; sourceTree = ""; }; + 033A9EDA252C17F200BC26E1 /* CustomVideoSourceMediaIO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomVideoSourceMediaIO.swift; sourceTree = ""; }; + 033A9EE0252C191000BC26E1 /* PrecallTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrecallTest.swift; sourceTree = ""; }; + 033A9F90252EA86A00BC26E1 /* CustomVideoSourcePush.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomVideoSourcePush.swift; sourceTree = ""; }; + 033A9F92252EA86A00BC26E1 /* CustomVideoRender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomVideoRender.swift; sourceTree = ""; }; + 033A9F94252EA86A00BC26E1 /* CustomAudioSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomAudioSource.swift; sourceTree = ""; }; + 033A9F96252EA86A00BC26E1 /* CustomAudioRender.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomAudioRender.swift; sourceTree = ""; }; + 033A9F9B252EA86A00BC26E1 /* RTMPStreaming.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RTMPStreaming.swift; sourceTree = ""; }; + 033A9F9D252EA86A00BC26E1 /* RawMediaData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RawMediaData.swift; sourceTree = ""; }; + 033A9FB2252EADF600BC26E1 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; }; + 033A9FB4252EAEB500BC26E1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/JoinChannelVideo.storyboard; sourceTree = ""; }; + 033A9FB9252EAEF700BC26E1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/JoinChannelAudio.storyboard; sourceTree = ""; }; + 033A9FBE252EB02600BC26E1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CustomAudioRender.storyboard; sourceTree = ""; }; + 033A9FC3252EB02D00BC26E1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CustomAudioSource.storyboard; sourceTree = ""; }; + 033A9FC8252EB03700BC26E1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CustomVideoRender.storyboard; sourceTree = ""; }; + 033A9FCD252EB03F00BC26E1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CustomVideoSourcePush.storyboard; sourceTree = ""; }; + 033A9FD2252EB04700BC26E1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/RawMediaData.storyboard; sourceTree = ""; }; + 033A9FD7252EB05200BC26E1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/RTMPStreaming.storyboard; sourceTree = ""; }; + 033A9FDA252EB05500BC26E1 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/RTMPStreaming.strings"; sourceTree = ""; }; + 033A9FDC252EB05A00BC26E1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/PrecallTest.storyboard; sourceTree = ""; }; + 033A9FDF252EB06100BC26E1 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/PrecallTest.strings"; sourceTree = ""; }; + 033A9FE1252EB58600BC26E1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CustomVideoSourceMediaIO.storyboard; sourceTree = ""; }; + 033A9FE6252EB59000BC26E1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/VoiceChanger.storyboard; sourceTree = ""; }; + 033A9FEA252EB5C500BC26E1 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/VoiceChanger.strings"; sourceTree = ""; }; + 033A9FEC252EB5CC00BC26E1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/AudioMixing.storyboard; sourceTree = ""; }; + 033A9FEF252EB5D000BC26E1 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/AudioMixing.strings"; sourceTree = ""; }; + 033A9FF1252EB5EB00BC26E1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ChannelMediaRelay.storyboard; sourceTree = ""; }; + 033A9FF4252EB5EE00BC26E1 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/ChannelMediaRelay.strings"; sourceTree = ""; }; + 033A9FF6252EB5F400BC26E1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/JoinMultiChannel.storyboard; sourceTree = ""; }; + 033A9FF9252EB5F800BC26E1 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/JoinMultiChannel.strings"; sourceTree = ""; }; + 033A9FFB252EB5FD00BC26E1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/ScreenShare.storyboard; sourceTree = ""; }; + 033AA000252EB60800BC26E1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/StreamEncryption.storyboard; sourceTree = ""; }; + 033AA003252EB60B00BC26E1 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/StreamEncryption.strings"; sourceTree = ""; }; + 033AA004252EBBEC00BC26E1 /* Localizable.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; + 034C626325257EA600296ECF /* GlobalSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalSettings.swift; sourceTree = ""; }; + 034C62662525857200296ECF /* JoinChannelAudio.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JoinChannelAudio.swift; sourceTree = ""; }; + 034C626B25259FC200296ECF /* JoinChannelVideo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JoinChannelVideo.swift; sourceTree = ""; }; + 034C62702525A35700296ECF /* StreamEncryption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StreamEncryption.swift; sourceTree = ""; }; + 034C62752525C68C00296ECF /* AgoraCustomEncryption.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AgoraCustomEncryption.mm; sourceTree = ""; }; + 034C62762525C68C00296ECF /* AgoraCustomEncryption.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AgoraCustomEncryption.h; sourceTree = ""; }; + 034C627A2526C43900296ECF /* ScreenShare.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScreenShare.swift; sourceTree = ""; }; + 034C62862528255F00296ECF /* WindowsCenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowsCenter.swift; sourceTree = ""; }; + 034C628925282D5D00296ECF /* JoinMultiChannel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JoinMultiChannel.swift; sourceTree = ""; }; + 034C628F2528327800296ECF /* ChannelMediaRelay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelMediaRelay.swift; sourceTree = ""; }; + 034C62922528474D00296ECF /* StatisticsInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatisticsInfo.swift; sourceTree = ""; }; + 034C629A25295F2800296ECF /* AudioMixing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioMixing.swift; sourceTree = ""; }; + 034C629E25297ABB00296ECF /* audioeffect.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = audioeffect.mp3; sourceTree = ""; }; + 034C629F25297ABB00296ECF /* audiomixing.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = audiomixing.mp3; sourceTree = ""; }; + 034C62A4252ABA5C00296ECF /* VoiceChanger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VoiceChanger.swift; sourceTree = ""; }; + 036D3A9924FA395E00B1D8DC /* KeyCenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyCenter.swift; sourceTree = ""; }; + 036D3A9D24FA3A1000B1D8DC /* LogUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogUtils.swift; sourceTree = ""; }; + 036D3A9F24FA40EB00B1D8DC /* VideoView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = VideoView.xib; sourceTree = ""; }; + 036D3AA124FAA00A00B1D8DC /* Configs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configs.swift; sourceTree = ""; }; + 03896D2C24F8A00F008593CD /* APIExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = APIExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 03896D2F24F8A00F008593CD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 03896D3124F8A00F008593CD /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 03896D3324F8A011008593CD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 03896D3624F8A011008593CD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 03896D3824F8A011008593CD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 03896D3924F8A011008593CD /* APIExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = APIExample.entitlements; sourceTree = ""; }; + 03896D3E24F8A011008593CD /* APIExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = APIExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 03896D4224F8A011008593CD /* APIExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIExampleTests.swift; sourceTree = ""; }; + 03896D4424F8A011008593CD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 03896D4924F8A011008593CD /* APIExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = APIExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 03896D4D24F8A011008593CD /* APIExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIExampleUITests.swift; sourceTree = ""; }; + 03896D4F24F8A011008593CD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 03B12DA3250E8F7F00E55818 /* AgoraExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AgoraExtension.swift; sourceTree = ""; }; + 03B321D724FC0D5D008EBD2C /* AgoraCameraSourceMediaIO.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AgoraCameraSourceMediaIO.swift; sourceTree = ""; }; + 0CA9B97F4DF8A31A030414B3 /* Pods_APIExampleTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_APIExampleTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1784955BB217D1790A452465 /* Pods-APIExampleTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-APIExampleTests.release.xcconfig"; path = "Target Support Files/Pods-APIExampleTests/Pods-APIExampleTests.release.xcconfig"; sourceTree = ""; }; + 4C8551EF6F12F734D8F7C1F5 /* Pods-APIExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-APIExample.release.xcconfig"; path = "Target Support Files/Pods-APIExample/Pods-APIExample.release.xcconfig"; sourceTree = ""; }; + 576459FF259B1C22007B1E30 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/CreateDataStream.strings"; sourceTree = ""; }; + 57645A01259B1C22007B1E30 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CreateDataStream.storyboard; sourceTree = ""; }; + 57645A02259B1C22007B1E30 /* CreateDataStream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateDataStream.swift; sourceTree = ""; }; + 5770E2D3258C9E6F00812A80 /* Picker.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Picker.xib; sourceTree = ""; }; + 5770E2DE258CDCA600812A80 /* Picker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Picker.swift; sourceTree = ""; }; + 57887A68258856B7006E962A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Settings.storyboard; sourceTree = ""; }; + 57887A74258859D8006E962A /* SettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsController.swift; sourceTree = ""; }; + 57887A82258886E1006E962A /* SettingCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingCells.swift; sourceTree = ""; }; + 57887A86258889ED006E962A /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; + 57A635B425906D0500EDC2F7 /* Input.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Input.xib; sourceTree = ""; }; + 57A635BA25906D5500EDC2F7 /* Input.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Input.swift; sourceTree = ""; }; + 57A635D72591BC0C00EDC2F7 /* Slider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Slider.swift; sourceTree = ""; }; + 57A635DB2591BCF000EDC2F7 /* Slider.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = Slider.xib; sourceTree = ""; }; + 57A635E42591EDFA00EDC2F7 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/JoinChannelAudio.strings"; sourceTree = ""; }; + 57A635F32593544600EDC2F7 /* effectA.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = effectA.wav; sourceTree = ""; }; + 57AF397A259B31AA00601E02 /* RawAudioData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawAudioData.swift; sourceTree = ""; }; + 57AF3980259B329B00601E02 /* RawAudioData.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = RawAudioData.storyboard; sourceTree = ""; }; + 6F65EF2B97B89DE4581B426B /* Pods_APIExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_APIExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 84C863718A380DFD36ABF19F /* Pods-APIExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-APIExample.debug.xcconfig"; path = "Target Support Files/Pods-APIExample/Pods-APIExample.debug.xcconfig"; sourceTree = ""; }; + B53F41CB5AC550EA43C47363 /* Pods-APIExampleTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-APIExampleTests.debug.xcconfig"; path = "Target Support Files/Pods-APIExampleTests/Pods-APIExampleTests.debug.xcconfig"; sourceTree = ""; }; + B91A67063F1DBE9F621B114C /* Pods-APIExample-APIExampleUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-APIExample-APIExampleUITests.release.xcconfig"; path = "Target Support Files/Pods-APIExample-APIExampleUITests/Pods-APIExample-APIExampleUITests.release.xcconfig"; sourceTree = ""; }; + DC004435A834772C836F5662 /* Pods-APIExample-APIExampleUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-APIExample-APIExampleUITests.debug.xcconfig"; path = "Target Support Files/Pods-APIExample-APIExampleUITests/Pods-APIExample-APIExampleUITests.debug.xcconfig"; sourceTree = ""; }; + FC2BAB0AC82140B7CEEA31DA /* Pods_APIExample_APIExampleUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_APIExample_APIExampleUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 03896D2924F8A00F008593CD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 596A9F79AF0CD8DC1CA93253 /* Pods_APIExample.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03896D3B24F8A011008593CD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4667B9F286200B077FFDFDE1 /* Pods_APIExampleTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03896D4624F8A011008593CD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + EBDD0209B272C276B21B6270 /* Pods_APIExample_APIExampleUITests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 03267E1D2500C265004A91A6 /* RawDataApi */ = { + isa = PBXGroup; + children = ( + 03267E1E2500C265004A91A6 /* AgoraMediaRawData.h */, + 03267E202500C265004A91A6 /* AgoraMediaRawData.m */, + 03267E212500C265004A91A6 /* AgoraMediaDataPlugin.h */, + 03267E1F2500C265004A91A6 /* AgoraMediaDataPlugin.mm */, + ); + path = RawDataApi; + sourceTree = ""; + }; + 0333E63824FA335C0063C5B0 /* Examples */ = { + isa = PBXGroup; + children = ( + 0333E63924FA335C0063C5B0 /* Basic */, + 036D3AA524FB797700B1D8DC /* Advanced */, + ); + path = Examples; + sourceTree = ""; + }; + 0333E63924FA335C0063C5B0 /* Basic */ = { + isa = PBXGroup; + children = ( + 034C626A25259FC200296ECF /* JoinChannelVideo */, + 034C62652525857200296ECF /* JoinChannelAudio */, + ); + path = Basic; + sourceTree = ""; + }; + 0336A1BB25034F4600D61B7F /* ExternalAudio */ = { + isa = PBXGroup; + children = ( + 0336A1BC25034F4600D61B7F /* AudioOptions.h */, + 0336A1C325034F4700D61B7F /* AudioWriteToFile.h */, + 0336A1BD25034F4600D61B7F /* AudioWriteToFile.m */, + 0336A1BE25034F4600D61B7F /* ExternalAudio.h */, + 0336A1C225034F4700D61B7F /* ExternalAudio.mm */, + 0336A1BF25034F4700D61B7F /* AudioController.h */, + 0336A1C425034F4700D61B7F /* AudioController.m */, + ); + path = ExternalAudio; + sourceTree = ""; + }; + 033A9ED9252C17F200BC26E1 /* CustomVideoSourceMediaIO */ = { + isa = PBXGroup; + children = ( + 033A9EDA252C17F200BC26E1 /* CustomVideoSourceMediaIO.swift */, + 033A9FE2252EB58600BC26E1 /* CustomVideoSourceMediaIO.storyboard */, + ); + path = CustomVideoSourceMediaIO; + sourceTree = ""; + }; + 033A9EDE252C191000BC26E1 /* PrecallTest */ = { + isa = PBXGroup; + children = ( + 033A9FDD252EB05A00BC26E1 /* PrecallTest.storyboard */, + 033A9EE0252C191000BC26E1 /* PrecallTest.swift */, + ); + path = PrecallTest; + sourceTree = ""; + }; + 033A9F8F252EA86A00BC26E1 /* CustomVideoSourcePush */ = { + isa = PBXGroup; + children = ( + 033A9F90252EA86A00BC26E1 /* CustomVideoSourcePush.swift */, + 033A9FCE252EB03F00BC26E1 /* CustomVideoSourcePush.storyboard */, + ); + path = CustomVideoSourcePush; + sourceTree = ""; + }; + 033A9F91252EA86A00BC26E1 /* CustomVideoRender */ = { + isa = PBXGroup; + children = ( + 033A9F92252EA86A00BC26E1 /* CustomVideoRender.swift */, + 033A9FC9252EB03700BC26E1 /* CustomVideoRender.storyboard */, + ); + path = CustomVideoRender; + sourceTree = ""; + }; + 033A9F93252EA86A00BC26E1 /* CustomAudioSource */ = { + isa = PBXGroup; + children = ( + 033A9F94252EA86A00BC26E1 /* CustomAudioSource.swift */, + 033A9FC4252EB02D00BC26E1 /* CustomAudioSource.storyboard */, + ); + path = CustomAudioSource; + sourceTree = ""; + }; + 033A9F95252EA86A00BC26E1 /* CustomAudioRender */ = { + isa = PBXGroup; + children = ( + 033A9F96252EA86A00BC26E1 /* CustomAudioRender.swift */, + 033A9FBF252EB02600BC26E1 /* CustomAudioRender.storyboard */, + ); + path = CustomAudioRender; + sourceTree = ""; + }; + 033A9F9A252EA86A00BC26E1 /* RTMPStreaming */ = { + isa = PBXGroup; + children = ( + 033A9F9B252EA86A00BC26E1 /* RTMPStreaming.swift */, + 033A9FD8252EB05200BC26E1 /* RTMPStreaming.storyboard */, + ); + path = RTMPStreaming; + sourceTree = ""; + }; + 033A9F9C252EA86A00BC26E1 /* RawMediaData */ = { + isa = PBXGroup; + children = ( + 033A9F9D252EA86A00BC26E1 /* RawMediaData.swift */, + 033A9FD3252EB04700BC26E1 /* RawMediaData.storyboard */, + ); + path = RawMediaData; + sourceTree = ""; + }; + 034C62652525857200296ECF /* JoinChannelAudio */ = { + isa = PBXGroup; + children = ( + 034C62662525857200296ECF /* JoinChannelAudio.swift */, + 033A9FBA252EAEF700BC26E1 /* JoinChannelAudio.storyboard */, + ); + path = JoinChannelAudio; + sourceTree = ""; + }; + 034C626A25259FC200296ECF /* JoinChannelVideo */ = { + isa = PBXGroup; + children = ( + 034C626B25259FC200296ECF /* JoinChannelVideo.swift */, + 033A9FB5252EAEB500BC26E1 /* JoinChannelVideo.storyboard */, + ); + path = JoinChannelVideo; + sourceTree = ""; + }; + 034C626F2525A35700296ECF /* StreamEncryption */ = { + isa = PBXGroup; + children = ( + 034C62702525A35700296ECF /* StreamEncryption.swift */, + 033AA001252EB60800BC26E1 /* StreamEncryption.storyboard */, + ); + path = StreamEncryption; + sourceTree = ""; + }; + 034C62742525C68C00296ECF /* CustomEncryption */ = { + isa = PBXGroup; + children = ( + 034C62752525C68C00296ECF /* AgoraCustomEncryption.mm */, + 034C62762525C68C00296ECF /* AgoraCustomEncryption.h */, + ); + path = CustomEncryption; + sourceTree = ""; + }; + 034C62782526C43900296ECF /* ScreenShare */ = { + isa = PBXGroup; + children = ( + 033A9FFC252EB5FD00BC26E1 /* ScreenShare.storyboard */, + 034C627A2526C43900296ECF /* ScreenShare.swift */, + ); + path = ScreenShare; + sourceTree = ""; + }; + 034C628825282D5D00296ECF /* JoinMultiChannel */ = { + isa = PBXGroup; + children = ( + 034C628925282D5D00296ECF /* JoinMultiChannel.swift */, + 033A9FF7252EB5F400BC26E1 /* JoinMultiChannel.storyboard */, + ); + path = JoinMultiChannel; + sourceTree = ""; + }; + 034C628D2528327800296ECF /* ChannelMediaRelay */ = { + isa = PBXGroup; + children = ( + 033A9FF2252EB5EB00BC26E1 /* ChannelMediaRelay.storyboard */, + 034C628F2528327800296ECF /* ChannelMediaRelay.swift */, + ); + path = ChannelMediaRelay; + sourceTree = ""; + }; + 034C629425295F0700296ECF /* AudioMixing */ = { + isa = PBXGroup; + children = ( + 033A9FED252EB5CC00BC26E1 /* AudioMixing.storyboard */, + 034C629A25295F2800296ECF /* AudioMixing.swift */, + ); + path = AudioMixing; + sourceTree = ""; + }; + 034C629D25297ABB00296ECF /* Resources */ = { + isa = PBXGroup; + children = ( + 57A635F32593544600EDC2F7 /* effectA.wav */, + 034C629E25297ABB00296ECF /* audioeffect.mp3 */, + 034C629F25297ABB00296ECF /* audiomixing.mp3 */, + ); + path = Resources; + sourceTree = ""; + }; + 034C62A2252ABA5C00296ECF /* VoiceChanger */ = { + isa = PBXGroup; + children = ( + 033A9FE7252EB59000BC26E1 /* VoiceChanger.storyboard */, + 034C62A4252ABA5C00296ECF /* VoiceChanger.swift */, + ); + path = VoiceChanger; + sourceTree = ""; + }; + 036D3AA524FB797700B1D8DC /* Advanced */ = { + isa = PBXGroup; + children = ( + 57AF3979259B30BB00601E02 /* RawAudioData */, + 576459FD259B1C22007B1E30 /* CreateDataStream */, + 033A9F95252EA86A00BC26E1 /* CustomAudioRender */, + 033A9F93252EA86A00BC26E1 /* CustomAudioSource */, + 033A9F91252EA86A00BC26E1 /* CustomVideoRender */, + 033A9F8F252EA86A00BC26E1 /* CustomVideoSourcePush */, + 033A9F9C252EA86A00BC26E1 /* RawMediaData */, + 033A9F9A252EA86A00BC26E1 /* RTMPStreaming */, + 033A9EDE252C191000BC26E1 /* PrecallTest */, + 033A9ED9252C17F200BC26E1 /* CustomVideoSourceMediaIO */, + 034C62A2252ABA5C00296ECF /* VoiceChanger */, + 034C629425295F0700296ECF /* AudioMixing */, + 034C628D2528327800296ECF /* ChannelMediaRelay */, + 034C628825282D5D00296ECF /* JoinMultiChannel */, + 034C62782526C43900296ECF /* ScreenShare */, + 034C626F2525A35700296ECF /* StreamEncryption */, + ); + path = Advanced; + sourceTree = ""; + }; + 03896D2324F8A00F008593CD = { + isa = PBXGroup; + children = ( + 03896D2E24F8A00F008593CD /* APIExample */, + 03896D4124F8A011008593CD /* APIExampleTests */, + 03896D4C24F8A011008593CD /* APIExampleUITests */, + 03896D2D24F8A00F008593CD /* Products */, + 72510F6AF209B24C1F66A819 /* Pods */, + E8D399FF8F860CE7DAAA9D91 /* Frameworks */, + ); + sourceTree = ""; + }; + 03896D2D24F8A00F008593CD /* Products */ = { + isa = PBXGroup; + children = ( + 03896D2C24F8A00F008593CD /* APIExample.app */, + 03896D3E24F8A011008593CD /* APIExampleTests.xctest */, + 03896D4924F8A011008593CD /* APIExampleUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 03896D2E24F8A00F008593CD /* APIExample */ = { + isa = PBXGroup; + children = ( + 033AA004252EBBEC00BC26E1 /* Localizable.strings */, + 034C629D25297ABB00296ECF /* Resources */, + 03267E262500C779004A91A6 /* APIExample-Bridging-Header.h */, + 0333E63824FA335C0063C5B0 /* Examples */, + 03896D2F24F8A00F008593CD /* AppDelegate.swift */, + 03896D5B24F8D437008593CD /* Commons */, + 03896D3124F8A00F008593CD /* ViewController.swift */, + 03896D3324F8A011008593CD /* Assets.xcassets */, + 03896D3524F8A011008593CD /* Main.storyboard */, + 03896D3824F8A011008593CD /* Info.plist */, + 03896D3924F8A011008593CD /* APIExample.entitlements */, + 57887A69258856B7006E962A /* Settings.storyboard */, + 57887A74258859D8006E962A /* SettingsController.swift */, + ); + path = APIExample; + sourceTree = ""; + }; + 03896D4124F8A011008593CD /* APIExampleTests */ = { + isa = PBXGroup; + children = ( + 03896D4224F8A011008593CD /* APIExampleTests.swift */, + 03896D4424F8A011008593CD /* Info.plist */, + ); + path = APIExampleTests; + sourceTree = ""; + }; + 03896D4C24F8A011008593CD /* APIExampleUITests */ = { + isa = PBXGroup; + children = ( + 03896D4D24F8A011008593CD /* APIExampleUITests.swift */, + 03896D4F24F8A011008593CD /* Info.plist */, + ); + path = APIExampleUITests; + sourceTree = ""; + }; + 03896D5B24F8D437008593CD /* Commons */ = { + isa = PBXGroup; + children = ( + 5770E2C0258C580E00812A80 /* Component */, + 57887A7F25885FC2006E962A /* Settings */, + 034C62922528474D00296ECF /* StatisticsInfo.swift */, + 034C62862528255F00296ECF /* WindowsCenter.swift */, + 034C626325257EA600296ECF /* GlobalSettings.swift */, + 03B12DA3250E8F7F00E55818 /* AgoraExtension.swift */, + 034C62742525C68C00296ECF /* CustomEncryption */, + 0336A1BB25034F4600D61B7F /* ExternalAudio */, + 03267E1D2500C265004A91A6 /* RawDataApi */, + 03B321D424FC0D5D008EBD2C /* ExternalVideo */, + 036D3A9D24FA3A1000B1D8DC /* LogUtils.swift */, + 036D3A9924FA395E00B1D8DC /* KeyCenter.swift */, + 0333E63624FA32000063C5B0 /* VideoView.swift */, + 036D3A9F24FA40EB00B1D8DC /* VideoView.xib */, + 0301D31C2507C0F300DF3BEA /* MetalVideoView.xib */, + 0333E63424FA30310063C5B0 /* BaseViewController.swift */, + 036D3AA124FAA00A00B1D8DC /* Configs.swift */, + ); + path = Commons; + sourceTree = ""; + }; + 03B321D424FC0D5D008EBD2C /* ExternalVideo */ = { + isa = PBXGroup; + children = ( + 0301D3172507B4A800DF3BEA /* AgoraMetalRender.swift */, + 0301D3162507B4A800DF3BEA /* AgoraMetalShader.metal */, + 03267E1B24FF3AF4004A91A6 /* AgoraCameraSourcePush.swift */, + 03B321D724FC0D5D008EBD2C /* AgoraCameraSourceMediaIO.swift */, + ); + path = ExternalVideo; + sourceTree = ""; + }; + 576459FD259B1C22007B1E30 /* CreateDataStream */ = { + isa = PBXGroup; + children = ( + 576459FE259B1C22007B1E30 /* CreateDataStream.strings */, + 57645A00259B1C22007B1E30 /* CreateDataStream.storyboard */, + 57645A02259B1C22007B1E30 /* CreateDataStream.swift */, + ); + path = CreateDataStream; + sourceTree = ""; + }; + 5770E2C0258C580E00812A80 /* Component */ = { + isa = PBXGroup; + children = ( + 5770E2E2259040F900812A80 /* Base */, + ); + path = Component; + sourceTree = ""; + }; + 5770E2E2259040F900812A80 /* Base */ = { + isa = PBXGroup; + children = ( + 5770E2DE258CDCA600812A80 /* Picker.swift */, + 5770E2D3258C9E6F00812A80 /* Picker.xib */, + 57A635B425906D0500EDC2F7 /* Input.xib */, + 57A635BA25906D5500EDC2F7 /* Input.swift */, + 57A635D72591BC0C00EDC2F7 /* Slider.swift */, + 57A635DB2591BCF000EDC2F7 /* Slider.xib */, + ); + path = Base; + sourceTree = ""; + }; + 57887A7F25885FC2006E962A /* Settings */ = { + isa = PBXGroup; + children = ( + 57887A82258886E1006E962A /* SettingCells.swift */, + 57887A86258889ED006E962A /* SettingsViewController.swift */, + ); + path = Settings; + sourceTree = ""; + }; + 57AF3979259B30BB00601E02 /* RawAudioData */ = { + isa = PBXGroup; + children = ( + 57AF397A259B31AA00601E02 /* RawAudioData.swift */, + 57AF3980259B329B00601E02 /* RawAudioData.storyboard */, + ); + path = RawAudioData; + sourceTree = ""; + }; + 72510F6AF209B24C1F66A819 /* Pods */ = { + isa = PBXGroup; + children = ( + 84C863718A380DFD36ABF19F /* Pods-APIExample.debug.xcconfig */, + 4C8551EF6F12F734D8F7C1F5 /* Pods-APIExample.release.xcconfig */, + DC004435A834772C836F5662 /* Pods-APIExample-APIExampleUITests.debug.xcconfig */, + B91A67063F1DBE9F621B114C /* Pods-APIExample-APIExampleUITests.release.xcconfig */, + B53F41CB5AC550EA43C47363 /* Pods-APIExampleTests.debug.xcconfig */, + 1784955BB217D1790A452465 /* Pods-APIExampleTests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + E8D399FF8F860CE7DAAA9D91 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 6F65EF2B97B89DE4581B426B /* Pods_APIExample.framework */, + FC2BAB0AC82140B7CEEA31DA /* Pods_APIExample_APIExampleUITests.framework */, + 0CA9B97F4DF8A31A030414B3 /* Pods_APIExampleTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 03896D2B24F8A00F008593CD /* APIExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 03896D5224F8A011008593CD /* Build configuration list for PBXNativeTarget "APIExample" */; + buildPhases = ( + 8DA7D6BCFD639FBD281C7854 /* [CP] Check Pods Manifest.lock */, + 03896D2824F8A00F008593CD /* Sources */, + 03896D2924F8A00F008593CD /* Frameworks */, + 03896D2A24F8A00F008593CD /* Resources */, + 9B15FD1F702D590221B4E441 /* [CP] Embed Pods Frameworks */, + 032C0FA2254873AC00D80A57 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = APIExample; + productName = APIExample; + productReference = 03896D2C24F8A00F008593CD /* APIExample.app */; + productType = "com.apple.product-type.application"; + }; + 03896D3D24F8A011008593CD /* APIExampleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 03896D5524F8A011008593CD /* Build configuration list for PBXNativeTarget "APIExampleTests" */; + buildPhases = ( + EEFD79D3C6F65390F7C3779B /* [CP] Check Pods Manifest.lock */, + 03896D3A24F8A011008593CD /* Sources */, + 03896D3B24F8A011008593CD /* Frameworks */, + 03896D3C24F8A011008593CD /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 03896D4024F8A011008593CD /* PBXTargetDependency */, + ); + name = APIExampleTests; + productName = APIExampleTests; + productReference = 03896D3E24F8A011008593CD /* APIExampleTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 03896D4824F8A011008593CD /* APIExampleUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 03896D5824F8A011008593CD /* Build configuration list for PBXNativeTarget "APIExampleUITests" */; + buildPhases = ( + 628E81C7FFD436AD6CF8BE08 /* [CP] Check Pods Manifest.lock */, + 03896D4524F8A011008593CD /* Sources */, + 03896D4624F8A011008593CD /* Frameworks */, + 03896D4724F8A011008593CD /* Resources */, + 901130C80A2E08AA244B275B /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 03896D4B24F8A011008593CD /* PBXTargetDependency */, + ); + name = APIExampleUITests; + productName = APIExampleUITests; + productReference = 03896D4924F8A011008593CD /* APIExampleUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 03896D2424F8A00F008593CD /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1160; + LastUpgradeCheck = 1160; + ORGANIZATIONNAME = "Agora Corp"; + TargetAttributes = { + 03896D2B24F8A00F008593CD = { + CreatedOnToolsVersion = 11.6; + }; + 03896D3D24F8A011008593CD = { + CreatedOnToolsVersion = 11.6; + TestTargetID = 03896D2B24F8A00F008593CD; + }; + 03896D4824F8A011008593CD = { + CreatedOnToolsVersion = 11.6; + TestTargetID = 03896D2B24F8A00F008593CD; + }; + }; + }; + buildConfigurationList = 03896D2724F8A00F008593CD /* Build configuration list for PBXProject "APIExample" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + "zh-Hans", + ); + mainGroup = 03896D2324F8A00F008593CD; + productRefGroup = 03896D2D24F8A00F008593CD /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 03896D2B24F8A00F008593CD /* APIExample */, + 03896D3D24F8A011008593CD /* APIExampleTests */, + 03896D4824F8A011008593CD /* APIExampleUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 03896D2A24F8A00F008593CD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 033A9FDB252EB05A00BC26E1 /* PrecallTest.storyboard in Resources */, + 033A9FFA252EB5FD00BC26E1 /* ScreenShare.storyboard in Resources */, + 57645A03259B1C22007B1E30 /* CreateDataStream.strings in Resources */, + 5770E2D5258C9E6F00812A80 /* Picker.xib in Resources */, + 033AA005252EBBEC00BC26E1 /* Localizable.strings in Resources */, + 57887A67258856B7006E962A /* Settings.storyboard in Resources */, + 033A9FFF252EB60800BC26E1 /* StreamEncryption.storyboard in Resources */, + 0301D31D2507C0F300DF3BEA /* MetalVideoView.xib in Resources */, + 033A9FB8252EAEF700BC26E1 /* JoinChannelAudio.storyboard in Resources */, + 57645A04259B1C22007B1E30 /* CreateDataStream.storyboard in Resources */, + 57A635DC2591BCF000EDC2F7 /* Slider.xib in Resources */, + 033A9FC2252EB02D00BC26E1 /* CustomAudioSource.storyboard in Resources */, + 57AF3981259B329B00601E02 /* RawAudioData.storyboard in Resources */, + 033A9FE5252EB59000BC26E1 /* VoiceChanger.storyboard in Resources */, + 033A9FBD252EB02600BC26E1 /* CustomAudioRender.storyboard in Resources */, + 034C62A025297ABB00296ECF /* audioeffect.mp3 in Resources */, + 03896D3424F8A011008593CD /* Assets.xcassets in Resources */, + 03896D3724F8A011008593CD /* Main.storyboard in Resources */, + 033A9FE0252EB58600BC26E1 /* CustomVideoSourceMediaIO.storyboard in Resources */, + 033A9FB3252EAEB500BC26E1 /* JoinChannelVideo.storyboard in Resources */, + 033A9FC7252EB03700BC26E1 /* CustomVideoRender.storyboard in Resources */, + 036D3AA024FA40EB00B1D8DC /* VideoView.xib in Resources */, + 033A9FEB252EB5CC00BC26E1 /* AudioMixing.storyboard in Resources */, + 033A9FCC252EB03F00BC26E1 /* CustomVideoSourcePush.storyboard in Resources */, + 57A635F42593544600EDC2F7 /* effectA.wav in Resources */, + 033A9FF5252EB5F400BC26E1 /* JoinMultiChannel.storyboard in Resources */, + 033A9FD6252EB05200BC26E1 /* RTMPStreaming.storyboard in Resources */, + 033A9FF0252EB5EB00BC26E1 /* ChannelMediaRelay.storyboard in Resources */, + 033A9FD1252EB04700BC26E1 /* RawMediaData.storyboard in Resources */, + 034C62A125297ABB00296ECF /* audiomixing.mp3 in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03896D3C24F8A011008593CD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03896D4724F8A011008593CD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 033A9FE8252EB59700BC26E1 /* VoiceChanger.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 628E81C7FFD436AD6CF8BE08 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-APIExample-APIExampleUITests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 8DA7D6BCFD639FBD281C7854 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-APIExample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 901130C80A2E08AA244B275B /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-APIExample-APIExampleUITests/Pods-APIExample-APIExampleUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-APIExample-APIExampleUITests/Pods-APIExample-APIExampleUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-APIExample-APIExampleUITests/Pods-APIExample-APIExampleUITests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9B15FD1F702D590221B4E441 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-APIExample/Pods-APIExample-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-APIExample/Pods-APIExample-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-APIExample/Pods-APIExample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + EEFD79D3C6F65390F7C3779B /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-APIExampleTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 03896D2824F8A00F008593CD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0301D3182507B4A800DF3BEA /* AgoraMetalShader.metal in Sources */, + 0333E63724FA32000063C5B0 /* VideoView.swift in Sources */, + 57887A83258886E1006E962A /* SettingCells.swift in Sources */, + 03B321DB24FC0D5E008EBD2C /* AgoraCameraSourceMediaIO.swift in Sources */, + 57645A05259B1C22007B1E30 /* CreateDataStream.swift in Sources */, + 034C628A25282D5D00296ECF /* JoinMultiChannel.swift in Sources */, + 034C62A6252ABA5C00296ECF /* VoiceChanger.swift in Sources */, + 033A9EDB252C17F200BC26E1 /* CustomVideoSourceMediaIO.swift in Sources */, + 033A9FA0252EA86A00BC26E1 /* CustomAudioSource.swift in Sources */, + 034C629C25295F2800296ECF /* AudioMixing.swift in Sources */, + 03267E222500C265004A91A6 /* AgoraMediaDataPlugin.mm in Sources */, + 036D3AA224FAA00A00B1D8DC /* Configs.swift in Sources */, + 03267E1C24FF3AF4004A91A6 /* AgoraCameraSourcePush.swift in Sources */, + 034C626C25259FC200296ECF /* JoinChannelVideo.swift in Sources */, + 033A9FA5252EA86A00BC26E1 /* RawMediaData.swift in Sources */, + 034C62772525C68D00296ECF /* AgoraCustomEncryption.mm in Sources */, + 03896D3224F8A00F008593CD /* ViewController.swift in Sources */, + 03896D3024F8A00F008593CD /* AppDelegate.swift in Sources */, + 034C626425257EA600296ECF /* GlobalSettings.swift in Sources */, + 0301D3192507B4A800DF3BEA /* AgoraMetalRender.swift in Sources */, + 036D3A9A24FA395E00B1D8DC /* KeyCenter.swift in Sources */, + 57AF397B259B31AA00601E02 /* RawAudioData.swift in Sources */, + 033A9F9F252EA86A00BC26E1 /* CustomVideoRender.swift in Sources */, + 0336A1CB25034F4700D61B7F /* AudioController.m in Sources */, + 034C62672525857200296ECF /* JoinChannelAudio.swift in Sources */, + 5770E2DF258CDCA600812A80 /* Picker.swift in Sources */, + 57887A87258889ED006E962A /* SettingsViewController.swift in Sources */, + 57A635D82591BC0C00EDC2F7 /* Slider.swift in Sources */, + 034C62932528474D00296ECF /* StatisticsInfo.swift in Sources */, + 033A9FA4252EA86A00BC26E1 /* RTMPStreaming.swift in Sources */, + 03267E232500C265004A91A6 /* AgoraMediaRawData.m in Sources */, + 03B12DA4250E8F7F00E55818 /* AgoraExtension.swift in Sources */, + 0336A1C725034F4700D61B7F /* AudioWriteToFile.m in Sources */, + 034C62872528255F00296ECF /* WindowsCenter.swift in Sources */, + 034C62912528327800296ECF /* ChannelMediaRelay.swift in Sources */, + 033A9F9E252EA86A00BC26E1 /* CustomVideoSourcePush.swift in Sources */, + 0336A1CA25034F4700D61B7F /* ExternalAudio.mm in Sources */, + 033A9FA1252EA86A00BC26E1 /* CustomAudioRender.swift in Sources */, + 034C627C2526C43900296ECF /* ScreenShare.swift in Sources */, + 034C62712525A35800296ECF /* StreamEncryption.swift in Sources */, + 57887A75258859D8006E962A /* SettingsController.swift in Sources */, + 036D3A9E24FA3A1000B1D8DC /* LogUtils.swift in Sources */, + 033A9EE2252C191000BC26E1 /* PrecallTest.swift in Sources */, + 57A635B525906D0500EDC2F7 /* Input.xib in Sources */, + 57A635BB25906D5500EDC2F7 /* Input.swift in Sources */, + 0333E63524FA30310063C5B0 /* BaseViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03896D3A24F8A011008593CD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03896D4324F8A011008593CD /* APIExampleTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 03896D4524F8A011008593CD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03896D4E24F8A011008593CD /* APIExampleUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 03896D4024F8A011008593CD /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 03896D2B24F8A00F008593CD /* APIExample */; + targetProxy = 03896D3F24F8A011008593CD /* PBXContainerItemProxy */; + }; + 03896D4B24F8A011008593CD /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 03896D2B24F8A00F008593CD /* APIExample */; + targetProxy = 03896D4A24F8A011008593CD /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 033A9FB5252EAEB500BC26E1 /* JoinChannelVideo.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 033A9FB4252EAEB500BC26E1 /* Base */, + ); + name = JoinChannelVideo.storyboard; + sourceTree = ""; + }; + 033A9FBA252EAEF700BC26E1 /* JoinChannelAudio.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 033A9FB9252EAEF700BC26E1 /* Base */, + 57A635E42591EDFA00EDC2F7 /* zh-Hans */, + ); + name = JoinChannelAudio.storyboard; + sourceTree = ""; + }; + 033A9FBF252EB02600BC26E1 /* CustomAudioRender.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 033A9FBE252EB02600BC26E1 /* Base */, + ); + name = CustomAudioRender.storyboard; + sourceTree = ""; + }; + 033A9FC4252EB02D00BC26E1 /* CustomAudioSource.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 033A9FC3252EB02D00BC26E1 /* Base */, + ); + name = CustomAudioSource.storyboard; + sourceTree = ""; + }; + 033A9FC9252EB03700BC26E1 /* CustomVideoRender.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 033A9FC8252EB03700BC26E1 /* Base */, + ); + name = CustomVideoRender.storyboard; + sourceTree = ""; + }; + 033A9FCE252EB03F00BC26E1 /* CustomVideoSourcePush.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 033A9FCD252EB03F00BC26E1 /* Base */, + ); + name = CustomVideoSourcePush.storyboard; + sourceTree = ""; + }; + 033A9FD3252EB04700BC26E1 /* RawMediaData.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 033A9FD2252EB04700BC26E1 /* Base */, + ); + name = RawMediaData.storyboard; + sourceTree = ""; + }; + 033A9FD8252EB05200BC26E1 /* RTMPStreaming.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 033A9FD7252EB05200BC26E1 /* Base */, + 033A9FDA252EB05500BC26E1 /* zh-Hans */, + ); + name = RTMPStreaming.storyboard; + sourceTree = ""; + }; + 033A9FDD252EB05A00BC26E1 /* PrecallTest.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 033A9FDC252EB05A00BC26E1 /* Base */, + 033A9FDF252EB06100BC26E1 /* zh-Hans */, + ); + name = PrecallTest.storyboard; + sourceTree = ""; + }; + 033A9FE2252EB58600BC26E1 /* CustomVideoSourceMediaIO.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 033A9FE1252EB58600BC26E1 /* Base */, + ); + name = CustomVideoSourceMediaIO.storyboard; + sourceTree = ""; + }; + 033A9FE7252EB59000BC26E1 /* VoiceChanger.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 033A9FE6252EB59000BC26E1 /* Base */, + 033A9FEA252EB5C500BC26E1 /* zh-Hans */, + ); + name = VoiceChanger.storyboard; + sourceTree = ""; + }; + 033A9FED252EB5CC00BC26E1 /* AudioMixing.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 033A9FEC252EB5CC00BC26E1 /* Base */, + 033A9FEF252EB5D000BC26E1 /* zh-Hans */, + ); + name = AudioMixing.storyboard; + sourceTree = ""; + }; + 033A9FF2252EB5EB00BC26E1 /* ChannelMediaRelay.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 033A9FF1252EB5EB00BC26E1 /* Base */, + 033A9FF4252EB5EE00BC26E1 /* zh-Hans */, + ); + name = ChannelMediaRelay.storyboard; + sourceTree = ""; + }; + 033A9FF7252EB5F400BC26E1 /* JoinMultiChannel.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 033A9FF6252EB5F400BC26E1 /* Base */, + 033A9FF9252EB5F800BC26E1 /* zh-Hans */, + ); + name = JoinMultiChannel.storyboard; + sourceTree = ""; + }; + 033A9FFC252EB5FD00BC26E1 /* ScreenShare.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 033A9FFB252EB5FD00BC26E1 /* Base */, + ); + name = ScreenShare.storyboard; + sourceTree = ""; + }; + 033AA001252EB60800BC26E1 /* StreamEncryption.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 033AA000252EB60800BC26E1 /* Base */, + 033AA003252EB60B00BC26E1 /* zh-Hans */, + ); + name = StreamEncryption.storyboard; + sourceTree = ""; + }; + 03896D3524F8A011008593CD /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 03896D3624F8A011008593CD /* Base */, + 033A9FB2252EADF600BC26E1 /* zh-Hans */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 576459FE259B1C22007B1E30 /* CreateDataStream.strings */ = { + isa = PBXVariantGroup; + children = ( + 576459FF259B1C22007B1E30 /* zh-Hans */, + ); + name = CreateDataStream.strings; + sourceTree = ""; + }; + 57645A00259B1C22007B1E30 /* CreateDataStream.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 57645A01259B1C22007B1E30 /* Base */, + ); + name = CreateDataStream.storyboard; + sourceTree = ""; + }; + 57887A69258856B7006E962A /* Settings.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 57887A68258856B7006E962A /* Base */, + ); + name = Settings.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 03896D5024F8A011008593CD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 03896D5124F8A011008593CD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 03896D5324F8A011008593CD /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 84C863718A380DFD36ABF19F /* Pods-APIExample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = APIExample/APIExample.entitlements; + CODE_SIGN_IDENTITY = "Developer ID Application"; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = GM72UGLGZW; + ENABLE_HARDENED_RUNTIME = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/APIExample", + ); + INFOPLIST_FILE = APIExample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.agora.api.example.APIExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = apiexamplemac; + SWIFT_OBJC_BRIDGING_HEADER = "APIExample/APIExample-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 03896D5424F8A011008593CD /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4C8551EF6F12F734D8F7C1F5 /* Pods-APIExample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = APIExample/APIExample.entitlements; + CODE_SIGN_IDENTITY = "Developer ID Application"; + CODE_SIGN_STYLE = Manual; + COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = GM72UGLGZW; + ENABLE_HARDENED_RUNTIME = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/APIExample", + ); + INFOPLIST_FILE = APIExample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.agora.api.example.APIExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = apiexamplemac; + SWIFT_OBJC_BRIDGING_HEADER = "APIExample/APIExample-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 03896D5624F8A011008593CD /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B53F41CB5AC550EA43C47363 /* Pods-APIExampleTests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = APIExampleTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = io.agora.api.example.APIExampleTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/APIExample.app/Contents/MacOS/APIExample"; + }; + name = Debug; + }; + 03896D5724F8A011008593CD /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1784955BB217D1790A452465 /* Pods-APIExampleTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = APIExampleTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 10.15; + PRODUCT_BUNDLE_IDENTIFIER = io.agora.api.example.APIExampleTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/APIExample.app/Contents/MacOS/APIExample"; + }; + name = Release; + }; + 03896D5924F8A011008593CD /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DC004435A834772C836F5662 /* Pods-APIExample-APIExampleUITests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = APIExampleUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.agora.api.example.APIExampleUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = APIExample; + }; + name = Debug; + }; + 03896D5A24F8A011008593CD /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B91A67063F1DBE9F621B114C /* Pods-APIExample-APIExampleUITests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = APIExampleUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.agora.api.example.APIExampleUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = APIExample; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 03896D2724F8A00F008593CD /* Build configuration list for PBXProject "APIExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 03896D5024F8A011008593CD /* Debug */, + 03896D5124F8A011008593CD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 03896D5224F8A011008593CD /* Build configuration list for PBXNativeTarget "APIExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 03896D5324F8A011008593CD /* Debug */, + 03896D5424F8A011008593CD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 03896D5524F8A011008593CD /* Build configuration list for PBXNativeTarget "APIExampleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 03896D5624F8A011008593CD /* Debug */, + 03896D5724F8A011008593CD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 03896D5824F8A011008593CD /* Build configuration list for PBXNativeTarget "APIExampleUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 03896D5924F8A011008593CD /* Debug */, + 03896D5A24F8A011008593CD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 03896D2424F8A00F008593CD /* Project object */; +} diff --git a/macOS/APIExample/APIExample-Bridging-Header.h b/macOS/APIExample/APIExample-Bridging-Header.h new file mode 100644 index 000000000..a44765104 --- /dev/null +++ b/macOS/APIExample/APIExample-Bridging-Header.h @@ -0,0 +1,7 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import "AgoraMediaDataPlugin.h" +#import "ExternalAudio.h" +#import "AgoraCustomEncryption.h" diff --git a/macOS/APIExample/APIExample.entitlements b/macOS/APIExample/APIExample.entitlements new file mode 100644 index 000000000..6133db3ff --- /dev/null +++ b/macOS/APIExample/APIExample.entitlements @@ -0,0 +1,18 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.device.audio-input + + com.apple.security.device.camera + + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + + com.apple.security.network.server + + + diff --git a/macOS/APIExample/AppDelegate.swift b/macOS/APIExample/AppDelegate.swift new file mode 100644 index 000000000..026ae9bbb --- /dev/null +++ b/macOS/APIExample/AppDelegate.swift @@ -0,0 +1,26 @@ +// +// AppDelegate.swift +// APIExample +// +// Created by 张乾泽 on 2020/8/28. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import Cocoa + +@NSApplicationMain +class AppDelegate: NSObject, NSApplicationDelegate { + + + + func applicationDidFinishLaunching(_ aNotification: Notification) { + // Insert code here to initialize your application + } + + func applicationWillTerminate(_ aNotification: Notification) { + // Insert code here to tear down your application + } + + +} + diff --git a/macOS/APIExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/macOS/APIExample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..3f00db43e --- /dev/null +++ b/macOS/APIExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macOS/APIExample/Assets.xcassets/Contents.json b/macOS/APIExample/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/macOS/APIExample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/macOS/APIExample/Base.lproj/Main.storyboard b/macOS/APIExample/Base.lproj/Main.storyboard new file mode 100644 index 000000000..e69217dad --- /dev/null +++ b/macOS/APIExample/Base.lproj/Main.storyboard @@ -0,0 +1,880 @@ + + + + + + + + + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Base.lproj/Settings.storyboard b/macOS/APIExample/Base.lproj/Settings.storyboard new file mode 100644 index 000000000..b6b630862 --- /dev/null +++ b/macOS/APIExample/Base.lproj/Settings.storyboard @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Commons/AgoraExtension.swift b/macOS/APIExample/Commons/AgoraExtension.swift new file mode 100644 index 000000000..0537a2548 --- /dev/null +++ b/macOS/APIExample/Commons/AgoraExtension.swift @@ -0,0 +1,260 @@ +// +// AgoraCode.swift +// OpenLive +// +// Created by CavanSu on 2019/9/16. +// Copyright © 2019 Agora. All rights reserved. +// + +import AgoraRtcKit + +extension String { + var localized: String { NSLocalizedString(self, comment: "") } +} + +extension AgoraErrorCode { + var description: String { + var text: String + switch self { + case .joinChannelRejected: text = "join channel rejected" + case .leaveChannelRejected: text = "leave channel rejected" + case .invalidAppId: text = "invalid app id" + case .invalidToken: text = "invalid token" + case .invalidChannelId: text = "invalid channel id" + default: text = "\(self.rawValue)" + } + return text + } +} + +extension AgoraWarningCode { + var description: String { + var text: String + switch self { + case .invalidView: text = "invalid view" + default: text = "\(self.rawValue)" + } + return text + } +} + +extension AgoraNetworkQuality { + func description() -> String { + switch self { + case .excellent: return "excellent" + case .good: return "good" + case .poor: return "poor" + case .bad: return "bad" + case .vBad: return "very bad" + case .down: return "down" + case .unknown: return "unknown" + case .unsupported: return "unsupported" + case .detecting: return "detecting" + default: return "unknown" + } + } +} + +extension AgoraAudioProfile { + func description() -> String { + switch self { + case .default: return "Default".localized + case .musicStandard: return "Music Standard".localized + case .musicStandardStereo: return "Music Standard Stereo".localized + case .musicHighQuality: return "Music High Quality".localized + case .musicHighQualityStereo: return "Music High Quality Stereo".localized + case .speechStandard: return "Speech Standard".localized + default: + return "\(self.rawValue)" + } + } + static func allValues() -> [AgoraAudioProfile] { + return [.default, .speechStandard, .musicStandard, .musicStandardStereo, .musicHighQuality, .musicHighQualityStereo] + } +} + +extension AgoraClientRole { + func description() -> String { + switch self { + case .broadcaster: return "Broadcaster".localized + case .audience: return "Audience".localized + default: + return "\(self.rawValue)" + } + } + static func allValues() -> [AgoraClientRole] { + return [.broadcaster, .audience] + } +} + +extension AgoraAudioScenario { + func description() -> String { + switch self { + case .default: return "Default".localized + case .chatRoomGaming: return "Chat Room Gaming".localized + case .education: return "Education".localized + case .gameStreaming: return "Game Streaming".localized + case .chatRoomEntertainment: return "Chat Room Entertainment".localized + case .showRoom: return "Show Room".localized + default: + return "\(self.rawValue)" + } + } + + static func allValues() -> [AgoraAudioScenario] { + return [.default, .chatRoomGaming, .education, .gameStreaming, .chatRoomEntertainment, .showRoom] + } +} + +extension AgoraEncryptionMode { + func description() -> String { + switch self { + case .AES128XTS: return "AES128XTS" + case .AES256XTS: return "AES256XTS" + case .AES128ECB: return "AES128ECB" + case .SM4128ECB: return "SM4128ECB" + default: + return "\(self.rawValue)" + } + } + + static func allValues() -> [AgoraEncryptionMode] { + return [.AES128XTS, .AES256XTS, .AES128ECB, .SM4128ECB] + } +} + +extension AgoraAudioVoiceChanger { + func description() -> String { + switch self { + case .voiceChangerOff:return "Off".localized + case .generalBeautyVoiceFemaleFresh:return "FemaleFresh".localized + case .generalBeautyVoiceFemaleVitality:return "FemaleVitality".localized + case .generalBeautyVoiceMaleMagnetic:return "MaleMagnetic".localized + case .voiceBeautyVigorous:return "Vigorous".localized + case .voiceBeautyDeep:return "Deep".localized + case .voiceBeautyMellow:return "Mellow".localized + case .voiceBeautyFalsetto:return "Falsetto".localized + case .voiceBeautyFull:return "Full".localized + case .voiceBeautyClear:return "Clear".localized + case .voiceBeautyResounding:return "Resounding".localized + case .voiceBeautyRinging:return "Ringing".localized + case .voiceBeautySpacial:return "Spacial".localized + case .voiceChangerEthereal:return "Ethereal".localized + case .voiceChangerOldMan:return "Old Man".localized + case .voiceChangerBabyBoy:return "Baby Boy".localized + case .voiceChangerBabyGirl:return "Baby Girl".localized + case .voiceChangerZhuBaJie:return "ZhuBaJie".localized + case .voiceChangerHulk:return "Hulk".localized + default: + return "\(self.rawValue)" + } + } +} + +extension AgoraVoiceBeautifierPreset{ + func description() -> String { + switch self { + case .voiceBeautifierOff:return "Off".localized + case .chatBeautifierFresh:return "FemaleFresh".localized + case .chatBeautifierMagnetic:return "MaleMagnetic".localized + case .chatBeautifierVitality:return "FemaleVitality".localized + case .timbreTransformationVigorous:return "Vigorous".localized + case .timbreTransformationDeep:return "Deep".localized + case .timbreTransformationMellow:return "Mellow".localized + case .timbreTransformationFalsetto:return "Falsetto".localized + case .timbreTransformationFull:return "Full".localized + case .timbreTransformationClear:return "Clear".localized + case .timbreTransformationResounding:return "Resounding".localized + case .timbreTransformationRinging:return "Ringing".localized + default: + return "\(self.rawValue)" + } + } +} + +extension AgoraAudioEffectPreset { + func description() -> String { + switch self { + case .audioEffectOff:return "Off".localized + case .voiceChangerEffectUncle:return "FxUncle".localized + case .voiceChangerEffectOldMan:return "Old Man".localized + case .voiceChangerEffectBoy:return "Baby Boy".localized + case .voiceChangerEffectSister:return "FxSister".localized + case .voiceChangerEffectGirl:return "Baby Girl".localized + case .voiceChangerEffectPigKing:return "ZhuBaJie".localized + case .voiceChangerEffectHulk:return "Hulk".localized + case .styleTransformationRnB:return "R&B".localized + case .styleTransformationPopular:return "Pop".localized + case .roomAcousticsKTV:return "KTV".localized + case .roomAcousticsVocalConcert:return "Vocal Concert".localized + case .roomAcousticsStudio:return "Studio".localized + case .roomAcousticsPhonograph:return "Phonograph".localized + case .roomAcousticsVirtualStereo:return "Virtual Stereo".localized + case .roomAcousticsSpacial:return "Spacial".localized + case .roomAcousticsEthereal:return "Ethereal".localized + case .roomAcoustics3DVoice:return "3D Voice".localized + case .pitchCorrection:return "Pitch Correction".localized + default: + return "\(self.rawValue)" + } + } +} + +extension AgoraAudioReverbPreset { + func description() -> String { + switch self { + case .off:return "Off".localized + case .fxUncle:return "FxUncle".localized + case .fxSister:return "FxSister".localized + case .fxPopular:return "Pop".localized + case .popular:return "Pop(Old Version)".localized + case .fxRNB:return "R&B".localized + case .rnB:return "R&B(Old Version)".localized + case .rock:return "Rock".localized + case .hipHop:return "HipHop".localized + case .fxVocalConcert:return "Vocal Concert".localized + case .vocalConcert:return "Vocal Concert(Old Version)".localized + case .fxKTV:return "KTV".localized + case .KTV:return "KTV(Old Version)".localized + case .fxStudio:return "Studio".localized + case .studio:return "Studio(Old Version)".localized + case .fxPhonograph:return "Phonograph".localized + case .virtualStereo:return "Virtual Stereo".localized + default: + return "\(self.rawValue)" + } + } +} + +extension AgoraAudioEqualizationBandFrequency { + func description() -> String { + switch self { + case .band31: return "31Hz" + case .band62: return "62Hz" + case .band125: return "125Hz" + case .band250: return "250Hz" + case .band500: return "500Hz" + case .band1K: return "1kHz" + case .band2K: return "2kHz" + case .band4K: return "4kHz" + case .band8K: return "8kHz" + case .band16K: return "16kHz" + @unknown default: + return "\(self.rawValue)" + } + } +} + +extension AgoraAudioReverbType { + func description() -> String { + switch self { + case .dryLevel: return "Dry Level".localized + case .wetLevel: return "Wet Level".localized + case .roomSize: return "Room Size".localized + case .wetDelay: return "Wet Delay".localized + case .strength: return "Strength".localized + @unknown default: + return "\(self.rawValue)" + } + } +} diff --git a/macOS/APIExample/Commons/BaseViewController.swift b/macOS/APIExample/Commons/BaseViewController.swift new file mode 100644 index 000000000..b75115b7f --- /dev/null +++ b/macOS/APIExample/Commons/BaseViewController.swift @@ -0,0 +1,122 @@ +// +// BaseVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import Cocoa +import AGEVideoLayout + +protocol BaseView: NSViewController { + func showAlert(title: String?, message: String) + func viewWillBeRemovedFromSplitView() +} + +class BaseViewController: NSViewController, BaseView { + var configs: [String:Any] = [:] + + func showAlert(title: String? = nil, message: String) { + let alert = NSAlert() + alert.alertStyle = .critical + alert.addButton(withTitle: "OK") + if let stitle = title { + alert.messageText = stitle + } + alert.informativeText = message + + alert.runModal() + } + + func getAudioLabel(uid:UInt, isLocal:Bool) -> String { + return "AUDIO ONLY\n\(isLocal ? "Local" : "Remote")\n\(uid)" + } + + func viewWillBeRemovedFromSplitView() {} +} + +extension AGEVideoContainer { + func layoutStream(views: [NSView]) { + let count = views.count + + var layout: AGEVideoLayout + + switch count { + case 1: + layout = AGEVideoLayout(level: 0) + .itemSize(.scale(CGSize(width: 1, height: 1))) + break + case 2: + layout = AGEVideoLayout(level: 0) + .itemSize(.scale(CGSize(width: 1, height: 0.5))) + break + case 4: + layout = AGEVideoLayout(level: 0) + .itemSize(.scale(CGSize(width: 0.5, height: 0.5))) + break + case 9: + layout = AGEVideoLayout(level: 0) + .itemSize(.scale(CGSize(width: 0.33, height: 0.33))) + break + case 16: + layout = AGEVideoLayout(level: 0) + .itemSize(.scale(CGSize(width: 0.25, height: 0.25))) + break + default: + return + } + + self.listCount { (level) -> Int in + return views.count + }.listItem { (index) -> AGEView in + return views[index.item] + } + + self.setLayouts([layout]) + } + + func layoutStream2(views: [NSView]) { + let count = views.count + + var layout: AGEVideoLayout + + switch count { + case 2: + layout = AGEVideoLayout(level: 0) + .itemSize(.scale(CGSize(width: 0.5, height: 1))) + break + default: + return + } + + self.listCount { (level) -> Int in + return views.count + }.listItem { (index) -> AGEView in + return views[index.item] + } + + self.setLayouts([layout]) + } + + func layoutStream3x3(views: [NSView]) { + let count = views.count + + var layout: AGEVideoLayout + + if count > 9 { + return + } else { + layout = AGEVideoLayout(level: 0) + .itemSize(.scale(CGSize(width: 0.33, height: 0.33))) + } + + self.listCount { (level) -> Int in + return views.count + }.listItem { (index) -> AGEView in + return views[index.item] + } + + self.setLayouts([layout]) + } +} diff --git a/macOS/APIExample/Commons/Component/Base/Input.swift b/macOS/APIExample/Commons/Component/Base/Input.swift new file mode 100644 index 000000000..34a961e04 --- /dev/null +++ b/macOS/APIExample/Commons/Component/Base/Input.swift @@ -0,0 +1,62 @@ +// +// Input.swift +// APIExample +// +// Created by XC on 2020/12/21. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import Cocoa + +class Input: NSView { + + @IBOutlet var contentView: NSView! + @IBOutlet weak var label: NSTextField! + @IBOutlet weak var field: NSTextField! + + var isEnabled: Bool { + get { + field.isEnabled + } + set { + field.isEnabled = newValue + } + } + + var stringValue: String { + get { + field.stringValue + } + set { + field.stringValue = newValue + } + } + + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + initUI() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + initUI() + } + + open func initUI() { + let bundle = Bundle(for: type(of: self)) + let nib = NSNib(nibNamed: .init("Input"), bundle: bundle)! + nib.instantiate(withOwner: self, topLevelObjects: nil) + + addSubview(contentView) + label.cell?.title = title() + field.placeholderString = placeholderString() + } + + open func title() -> String { + return "Label" + } + + open func placeholderString() -> String { + return "" + } +} diff --git a/macOS/APIExample/Commons/Component/Base/Input.xib b/macOS/APIExample/Commons/Component/Base/Input.xib new file mode 100644 index 000000000..c5863edf8 --- /dev/null +++ b/macOS/APIExample/Commons/Component/Base/Input.xib @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Commons/Component/Base/Picker.swift b/macOS/APIExample/Commons/Component/Base/Picker.swift new file mode 100644 index 000000000..f93d21340 --- /dev/null +++ b/macOS/APIExample/Commons/Component/Base/Picker.swift @@ -0,0 +1,67 @@ +// +// Picker.swift +// APIExample +// +// Created by XC on 2020/12/18. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import Cocoa + +class Picker: NSView { + + @IBOutlet var contentView: NSView! + @IBOutlet weak var label: NSTextField! + @IBOutlet weak var picker: NSPopUpButton! + + private var listener: (() -> Void)? + + var isEnabled: Bool { + get { + picker.isEnabled + } + set { + picker.isEnabled = newValue + } + } + + var indexOfSelectedItem: Int { + get { + picker.indexOfSelectedItem + } + } + + open func title() -> String { + return "Label" + } + + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + initUI() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + initUI() + } + + open func initUI() { + let bundle = Bundle(for: type(of: self)) + let nib = NSNib(nibNamed: .init("Picker"), bundle: bundle)! + nib.instantiate(withOwner: self, topLevelObjects: nil) + + addSubview(contentView) + label.cell?.title = title() + + self.picker.target = self + self.picker.action = #selector(onSelect) + } + + @IBAction open func onSelect(_ sender: NSPopUpButton) { + listener?() + } + + func onSelectChanged(_ callback: @escaping () -> Void) { + listener = callback + } +} diff --git a/macOS/APIExample/Commons/Component/Base/Picker.xib b/macOS/APIExample/Commons/Component/Base/Picker.xib new file mode 100644 index 000000000..525ec6cc3 --- /dev/null +++ b/macOS/APIExample/Commons/Component/Base/Picker.xib @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Commons/Component/Base/Slider.swift b/macOS/APIExample/Commons/Component/Base/Slider.swift new file mode 100644 index 000000000..7b2b26c40 --- /dev/null +++ b/macOS/APIExample/Commons/Component/Base/Slider.swift @@ -0,0 +1,61 @@ +// +// Slider.swift +// APIExample +// +// Created by XC on 2020/12/22. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import Cocoa + +class Slider: NSView { + + @IBOutlet var contentView: NSView! + @IBOutlet weak var label: NSTextField! + @IBOutlet weak var slider: NSSlider! + + private var listener: (() -> Void)? + + var isEnabled: Bool { + get { + slider.isEnabled + } + set { + slider.isEnabled = newValue + } + } + + open func title() -> String { + return "Label" + } + + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + initUI() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + initUI() + } + + open func initUI() { + let bundle = Bundle(for: type(of: self)) + let nib = NSNib(nibNamed: .init("Slider"), bundle: bundle)! + nib.instantiate(withOwner: self, topLevelObjects: nil) + + addSubview(contentView) + label.cell?.title = title() + + self.slider.target = self + self.slider.action = #selector(onChange) + } + + @IBAction open func onChange(_ sender: NSSlider) { + listener?() + } + + func onSliderChanged(_ callback: @escaping () -> Void) { + listener = callback + } +} diff --git a/macOS/APIExample/Commons/Component/Base/Slider.xib b/macOS/APIExample/Commons/Component/Base/Slider.xib new file mode 100644 index 000000000..df2dfd410 --- /dev/null +++ b/macOS/APIExample/Commons/Component/Base/Slider.xib @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Commons/Configs.swift b/macOS/APIExample/Commons/Configs.swift new file mode 100644 index 000000000..13df02c39 --- /dev/null +++ b/macOS/APIExample/Commons/Configs.swift @@ -0,0 +1,50 @@ +// +// Configs.swift +// APIExample +// +// Created by 张乾泽 on 2020/8/29. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import Foundation + +struct Resolution { + var width: Int + var height: Int + func name() -> String { + return "\(width)x\(height)" + } + func size() -> CGSize { + return CGSize(width: width, height: height) + } +} + +struct Layout { + let label: String + let value: Int + + init(_ k: String, _ v: Int) { + self.label = k + self.value = v + } +} + +class Configs { + static var defaultResolutionIdx: Int = 2 + static var Resolutions:[Resolution] = [ + Resolution(width: 320, height: 240), + Resolution(width: 640, height: 480), + Resolution(width: 960, height: 720), + Resolution(width: 1920, height: 1080) + ] + static var defaultFpsIdx: Int = 1 + static var Fps:[Int] = [ + 15, + 30 + ] + static var VideoContentHints:[AgoraVideoContentHint] = [ + AgoraVideoContentHint.none, + AgoraVideoContentHint.motion, + AgoraVideoContentHint.details + ] +} diff --git a/macOS/APIExample/Commons/CustomEncryption/AgoraCustomEncryption.h b/macOS/APIExample/Commons/CustomEncryption/AgoraCustomEncryption.h new file mode 100644 index 000000000..377019342 --- /dev/null +++ b/macOS/APIExample/Commons/CustomEncryption/AgoraCustomEncryption.h @@ -0,0 +1,18 @@ +// +// AgoraCustomEncryption.h +// AgoraRtcCustomizedEncryptionTutorial +// +// Created by suleyu on 2018/7/6. +// Copyright © 2018 Agora.io. All rights reserved. +// + +#import +#import + +@interface AgoraCustomEncryption : NSObject + ++ (void)registerPacketProcessing:(AgoraRtcEngineKit *)rtcEngineKit; + ++ (void)deregisterPacketProcessing:(AgoraRtcEngineKit *)rtcEngineKit; + +@end diff --git a/macOS/APIExample/Commons/CustomEncryption/AgoraCustomEncryption.mm b/macOS/APIExample/Commons/CustomEncryption/AgoraCustomEncryption.mm new file mode 100644 index 000000000..713c055e6 --- /dev/null +++ b/macOS/APIExample/Commons/CustomEncryption/AgoraCustomEncryption.mm @@ -0,0 +1,122 @@ +// +// AgoraCustomEncryption.m +// AgoraRtcCustomizedEncryptionTutorial +// +// Created by suleyu on 2018/7/6. +// Copyright © 2018 Agora.io. All rights reserved. +// + +#import "AgoraCustomEncryption.h" + +#include +#include + +class AgoraCustomEncryptionObserver : public agora::rtc::IPacketObserver +{ +public: + AgoraCustomEncryptionObserver() + { + m_txAudioBuffer.resize(2048); + m_rxAudioBuffer.resize(2048); + m_txVideoBuffer.resize(2048); + m_rxVideoBuffer.resize(2048); + } + virtual bool onSendAudioPacket(Packet& packet) + { + int i; + //encrypt the packet + const unsigned char* p = packet.buffer; + const unsigned char* pe = packet.buffer+packet.size; + + + for (i = 0; p < pe && i < m_txAudioBuffer.size(); ++p, ++i) + { + m_txAudioBuffer[i] = *p ^ 0x55; + } + //assign new buffer and the length back to SDK + packet.buffer = &m_txAudioBuffer[0]; + packet.size = i; + return true; + } + + virtual bool onSendVideoPacket(Packet& packet) + { + int i; + //encrypt the packet + const unsigned char* p = packet.buffer; + const unsigned char* pe = packet.buffer+packet.size; + for (i = 0; p < pe && i < m_txVideoBuffer.size(); ++p, ++i) + { + m_txVideoBuffer[i] = *p ^ 0x55; + } + //assign new buffer and the length back to SDK + packet.buffer = &m_txVideoBuffer[0]; + packet.size = i; + return true; + } + + virtual bool onReceiveAudioPacket(Packet& packet) + { + int i = 0; + //decrypt the packet + const unsigned char* p = packet.buffer; + const unsigned char* pe = packet.buffer+packet.size; + for (i = 0; p < pe && i < m_rxAudioBuffer.size(); ++p, ++i) + { + m_rxAudioBuffer[i] = *p ^ 0x55; + } + //assign new buffer and the length back to SDK + packet.buffer = &m_rxAudioBuffer[0]; + packet.size = i; + return true; + } + + virtual bool onReceiveVideoPacket(Packet& packet) + { + int i = 0; + //decrypt the packet + const unsigned char* p = packet.buffer; + const unsigned char* pe = packet.buffer+packet.size; + + + for (i = 0; p < pe && i < m_rxVideoBuffer.size(); ++p, ++i) + { + m_rxVideoBuffer[i] = *p ^ 0x55; + } + //assign new buffer and the length back to SDK + packet.buffer = &m_rxVideoBuffer[0]; + packet.size = i; + return true; + } + +private: + std::vector m_txAudioBuffer; //buffer for sending audio data + std::vector m_txVideoBuffer; //buffer for sending video data + + std::vector m_rxAudioBuffer; //buffer for receiving audio data + std::vector m_rxVideoBuffer; //buffer for receiving video data +}; + +static AgoraCustomEncryptionObserver s_packetObserver; + +@implementation AgoraCustomEncryption + ++ (void)registerPacketProcessing:(AgoraRtcEngineKit *)rtcEngineKit { + if (!rtcEngineKit) { + return; + } + + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)rtcEngineKit.getNativeHandle; + rtc_engine->registerPacketObserver(&s_packetObserver); +} + ++ (void)deregisterPacketProcessing:(AgoraRtcEngineKit *)rtcEngineKit { + if (!rtcEngineKit) { + return; + } + + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)rtcEngineKit.getNativeHandle; + rtc_engine->registerPacketObserver(NULL); +} + +@end diff --git a/macOS/APIExample/Commons/ExternalAudio/AudioController.h b/macOS/APIExample/Commons/ExternalAudio/AudioController.h new file mode 100644 index 000000000..4149e80b9 --- /dev/null +++ b/macOS/APIExample/Commons/ExternalAudio/AudioController.h @@ -0,0 +1,35 @@ +// +// AudioController.h +// AudioCapture +// +// Created by CavanSu on 10/11/2017. +// Copyright © 2017 Agora. All rights reserved. +// + +#import +#import +#import "AudioOptions.h" + +@class AudioController; +@protocol AudioControllerDelegate +@optional +- (void)audioController:(AudioController *)controller + didCaptureData:(unsigned char *)data + bytesLength:(int)bytesLength; +- (int)audioController:(AudioController *)controller + didRenderData:(unsigned char *)data + bytesLength:(int)bytesLength; +- (void)audioController:(AudioController *)controller + error:(OSStatus)error + info:(NSString *)info; +@end + + +@interface AudioController : NSObject +@property (nonatomic, weak) id delegate; + ++ (instancetype)audioController; +- (void)setUpAudioSessionWithSampleRate:(int)sampleRate channelCount:(int)channelCount audioCRMode:(AudioCRMode)audioCRMode IOType:(IOUnitType)ioType; +- (void)startWork; +- (void)stopWork; + @end diff --git a/macOS/APIExample/Commons/ExternalAudio/AudioController.m b/macOS/APIExample/Commons/ExternalAudio/AudioController.m new file mode 100644 index 000000000..1cd84fa85 --- /dev/null +++ b/macOS/APIExample/Commons/ExternalAudio/AudioController.m @@ -0,0 +1,417 @@ +// +// AudioController.m +// AudioCapture +// +// Created by CavanSu on 10/11/2017. +// Copyright © 2017 Agora. All rights reserved. +// + +#import "AudioController.h" +#import "AudioWriteToFile.h" + +#define InputBus 1 +#define OutputBus 0 + +@interface AudioController () +@property (nonatomic, assign) int sampleRate; +@property (nonatomic, assign) int channelCount; +@property (nonatomic, assign) AudioCRMode audioCRMode; +@property (nonatomic, assign) OSStatus error; + +@property (nonatomic, assign) AudioUnit remoteIOUnit; +#if TARGET_OS_MAC +@property (nonatomic, assign) AudioUnit macPlayUnit; +#endif +@end + +@implementation AudioController + +#if TARGET_OS_IPHONE +static double preferredIOBufferDuration = 0.02; +#endif + ++ (instancetype)audioController { + AudioController *audioController = [[self alloc] init]; + return audioController; +} + +#pragma mark - +static OSStatus captureCallBack(void *inRefCon, + AudioUnitRenderActionFlags *ioActionFlags, + const AudioTimeStamp *inTimeStamp, + UInt32 inBusNumber, // inputBus = 1 + UInt32 inNumberFrames, + AudioBufferList *ioData) +{ + AudioController *audioController = (__bridge AudioController *)inRefCon; + + AudioUnit captureUnit = [audioController remoteIOUnit]; + + if (!inRefCon) return 0; + + AudioBuffer buffer; + buffer.mData = NULL; + buffer.mDataByteSize = 0; + buffer.mNumberChannels = audioController.channelCount; + + AudioBufferList bufferList; + bufferList.mNumberBuffers = 1; + bufferList.mBuffers[0] = buffer; + + OSStatus status = AudioUnitRender(captureUnit, + ioActionFlags, + inTimeStamp, + inBusNumber, + inNumberFrames, + &bufferList); + + if (!status) { + if ([audioController.delegate respondsToSelector:@selector(audioController:didCaptureData:bytesLength:)]) { + [audioController.delegate audioController:audioController didCaptureData:(unsigned char *)bufferList.mBuffers[0].mData bytesLength:bufferList.mBuffers[0].mDataByteSize]; + } + } + else { + [audioController error:status position:@"captureCallBack"]; + } + + return 0; +} + +#pragma mark - +static OSStatus renderCallBack(void *inRefCon, + AudioUnitRenderActionFlags *ioActionFlags, + const AudioTimeStamp *inTimeStamp, + UInt32 inBusNumber, + UInt32 inNumberFrames, + AudioBufferList *ioData) +{ + AudioController *audioController = (__bridge AudioController *)(inRefCon); + + if (*ioActionFlags == kAudioUnitRenderAction_OutputIsSilence) { + return noErr; + } + + int result = 0; + + if ([audioController.delegate respondsToSelector:@selector(audioController:didRenderData:bytesLength:)]) { + result = [audioController.delegate audioController:audioController didRenderData:(uint8_t*)ioData->mBuffers[0].mData bytesLength:ioData->mBuffers[0].mDataByteSize]; + } + + if (result == 0) { + *ioActionFlags = kAudioUnitRenderAction_OutputIsSilence; + ioData->mBuffers[0].mDataByteSize = 0; + } + + return noErr; +} + + +#pragma mark - +- (void)setUpAudioSessionWithSampleRate:(int)sampleRate channelCount:(int)channelCount audioCRMode:(AudioCRMode)audioCRMode IOType:(IOUnitType)ioType{ + if (_audioCRMode == AudioCRModeSDKCaptureSDKRender) { + return; + } + + self.audioCRMode = audioCRMode; + self.sampleRate = sampleRate; + self.channelCount = channelCount; + +#if TARGET_OS_IPHONE + AVAudioSession *audioSession = [AVAudioSession sharedInstance]; + NSUInteger sessionOption = AVAudioSessionCategoryOptionMixWithOthers; + sessionOption |= AVAudioSessionCategoryOptionAllowBluetooth; + + [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:sessionOption error:nil]; + [audioSession setMode:AVAudioSessionModeDefault error:nil]; + [audioSession setPreferredIOBufferDuration:preferredIOBufferDuration error:nil]; + NSError *error; + BOOL success = [audioSession setActive:YES error:&error]; + if (!success) { + NSLog(@" audioSession setActive:YES error:nil"); + } + if (error) { + NSLog(@" setUpAudioSessionWithSampleRate : %@", error.localizedDescription); + } +#endif + + [self setupRemoteIOWithIOType:ioType]; +} + +#pragma mark - +- (void)setupRemoteIOWithIOType:(IOUnitType)ioType { +#if TARGET_OS_IPHONE + // AudioComponentDescription + AudioComponentDescription remoteIODesc; + remoteIODesc.componentType = kAudioUnitType_Output; + remoteIODesc.componentSubType = ioType == IOUnitTypeVPIO ? kAudioUnitSubType_VoiceProcessingIO : kAudioUnitSubType_RemoteIO; + remoteIODesc.componentManufacturer = kAudioUnitManufacturer_Apple; + remoteIODesc.componentFlags = 0; + remoteIODesc.componentFlagsMask = 0; + AudioComponent remoteIOComponent = AudioComponentFindNext(NULL, &remoteIODesc); + _error = AudioComponentInstanceNew(remoteIOComponent, &_remoteIOUnit); + [self error:_error position:@"AudioComponentInstanceNew"]; +#endif + + if (_audioCRMode == AudioCRModeExterCaptureSDKRender || _audioCRMode == AudioCRModeExterCaptureExterRender) { + +#if !TARGET_OS_IPHONE + AudioComponentDescription remoteIODesc; + remoteIODesc.componentType = kAudioUnitType_Output; + remoteIODesc.componentSubType = kAudioUnitSubType_HALOutput; + remoteIODesc.componentManufacturer = kAudioUnitManufacturer_Apple; + remoteIODesc.componentFlags = 0; + remoteIODesc.componentFlagsMask = 0; + AudioComponent remoteIOComponent = AudioComponentFindNext(NULL, &remoteIODesc); + _error = AudioComponentInstanceNew(remoteIOComponent, &_remoteIOUnit); + [self error:_error position:@"AudioComponentInstanceNew"]; + _error = AudioUnitInitialize(_remoteIOUnit); + [self error:_error position:@"AudioUnitInitialize"]; +#endif + [self setupCapture]; + } + + if (_audioCRMode == AudioCRModeSDKCaptureExterRender || _audioCRMode == AudioCRModeExterCaptureExterRender) { + +#if !TARGET_OS_IPHONE + AudioComponentDescription macPlayDesc; + macPlayDesc.componentType = kAudioUnitType_Output; + macPlayDesc.componentSubType = kAudioUnitSubType_DefaultOutput; + macPlayDesc.componentManufacturer = kAudioUnitManufacturer_Apple; + macPlayDesc.componentFlags = 0; + macPlayDesc.componentFlagsMask = 0; + AudioComponent macPlayComponent = AudioComponentFindNext(NULL, &macPlayDesc); + _error = AudioComponentInstanceNew(macPlayComponent, &_macPlayUnit); + [self error:_error position:@"AudioComponentInstanceNew"]; + _error = AudioUnitInitialize(_macPlayUnit); + [self error:_error position:@"AudioUnitInitialize"]; +#endif + [self setupRender]; + } + +} + +- (void)setupCapture { + // EnableIO + UInt32 one = 1; + _error = AudioUnitSetProperty(_remoteIOUnit, + kAudioOutputUnitProperty_EnableIO, + kAudioUnitScope_Input, + InputBus, + &one, + sizeof(one)); + [self error:_error position:@"kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input"]; + +#if !TARGET_OS_IPHONE + UInt32 disableFlag = 0; + + // Attention! set kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, disable + _error = AudioUnitSetProperty(_remoteIOUnit, + kAudioOutputUnitProperty_EnableIO, + kAudioUnitScope_Output, + OutputBus, + &disableFlag, + sizeof(disableFlag)); + [self error:_error position:@"kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output"]; + + AudioDeviceID defaultDevice = kAudioDeviceUnknown; + UInt32 propertySize = sizeof(defaultDevice); + AudioObjectPropertyAddress defaultDeviceProperty = { + .mSelector = kAudioHardwarePropertyDefaultInputDevice, + .mScope = kAudioObjectPropertyScopeInput, + .mElement = kAudioObjectPropertyElementMaster + }; + + _error = AudioObjectGetPropertyData(kAudioObjectSystemObject, + &defaultDeviceProperty, + 0, + NULL, + &propertySize, + &defaultDevice); + [self error:_error position:@"AudioObjectGetPropertyData, kAudioObjectSystemObject"]; + + // Set the sample rate of the input device to the output samplerate (if possible) + Float64 temp = _sampleRate; + defaultDeviceProperty.mSelector = kAudioDevicePropertyNominalSampleRate; + + _error = AudioObjectSetPropertyData(defaultDevice, + &defaultDeviceProperty, + 0, + NULL, + sizeof(Float64), + &temp); + [self error:_error position:@"AudioObjectSetPropertyData, defaultDeviceProperty"]; + + // Set the input device to the system's default input device + _error = AudioUnitSetProperty(_remoteIOUnit, + kAudioOutputUnitProperty_CurrentDevice, + kAudioUnitScope_Global, + InputBus, + &defaultDevice, + sizeof(defaultDevice)); + [self error:_error position:@"kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global"]; + +#endif + + // AudioStreamBasicDescription + AudioStreamBasicDescription streamFormatDesc = [self signedIntegerStreamFormatDesc]; + _error = AudioUnitSetProperty(_remoteIOUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Output, + InputBus, + &streamFormatDesc, + sizeof(streamFormatDesc)); + [self error:_error position:@"kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output"]; + + // CallBack + AURenderCallbackStruct captureCallBackStruck; + captureCallBackStruck.inputProcRefCon = (__bridge void * _Nullable)(self); + captureCallBackStruck.inputProc = captureCallBack; + + _error = AudioUnitSetProperty(_remoteIOUnit, + kAudioOutputUnitProperty_SetInputCallback, + kAudioUnitScope_Global, + InputBus, + &captureCallBackStruck, + sizeof(captureCallBackStruck)); + [self error:_error position:@"kAudioOutputUnitProperty_SetInputCallback"]; +} + +- (void)setupRender { + +#if TARGET_OS_IPHONE + // EnableIO + UInt32 one = 1; + _error = AudioUnitSetProperty(_remoteIOUnit, + kAudioOutputUnitProperty_EnableIO, + kAudioUnitScope_Output, + OutputBus, + &one, + sizeof(one)); + [self error:_error position:@"kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output"]; + + // AudioStreamBasicDescription + AudioStreamBasicDescription streamFormatDesc = [self signedIntegerStreamFormatDesc]; + _error = AudioUnitSetProperty(_remoteIOUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, + OutputBus, + &streamFormatDesc, + sizeof(streamFormatDesc)); + [self error:_error position:@"kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input"]; + + // CallBack + AURenderCallbackStruct renderCallback; + renderCallback.inputProcRefCon = (__bridge void * _Nullable)(self); + renderCallback.inputProc = renderCallBack; + AudioUnitSetProperty(_remoteIOUnit, + kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, + OutputBus, + &renderCallback, + sizeof(renderCallback)); + [self error:_error position:@"kAudioUnitProperty_SetRenderCallback"]; + +#else + + // AudioStreamBasicDescription + AudioStreamBasicDescription streamFormatDesc = [self signedIntegerStreamFormatDesc]; + _error = AudioUnitSetProperty(_macPlayUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, + OutputBus, + &streamFormatDesc, + sizeof(streamFormatDesc)); + [self error:_error position:@"kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input"]; + + // CallBack + AURenderCallbackStruct renderCallback; + renderCallback.inputProcRefCon = (__bridge void * _Nullable)(self); + renderCallback.inputProc = renderCallBack; + _error = AudioUnitSetProperty(_macPlayUnit, + kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, + OutputBus, + &renderCallback, + sizeof(renderCallback)); + [self error:_error position:@"kAudioUnitProperty_SetRenderCallback"]; +#endif + +} + +- (void)startWork { +#if TARGET_OS_IPHONE + _error = AudioOutputUnitStart(_remoteIOUnit); + [self error:_error position:@"AudioOutputUnitStart"]; +#else + if (_audioCRMode == AudioCRModeExterCaptureSDKRender || _audioCRMode == AudioCRModeExterCaptureExterRender) { + _error = AudioOutputUnitStart(_remoteIOUnit); + if (_error != noErr) { + [self error:_error position:@"AudioOutputUnitStart"]; + return; + } + } + + if (self.audioCRMode == AudioCRModeExterCaptureExterRender || self.audioCRMode == AudioCRModeSDKCaptureExterRender) { + _error = AudioOutputUnitStart(_macPlayUnit); + [self error:_error position:@"AudioOutputUnitStart"]; + } +#endif +} + +- (void)stopWork { +#if TARGET_OS_IPHONE + AudioOutputUnitStop(_remoteIOUnit); +#else + if (_audioCRMode == AudioCRModeExterCaptureSDKRender || _audioCRMode == AudioCRModeExterCaptureExterRender) { + AudioOutputUnitStop(_remoteIOUnit); + } + + if (self.audioCRMode == AudioCRModeExterCaptureExterRender || self.audioCRMode == AudioCRModeSDKCaptureExterRender) { + AudioOutputUnitStop(_macPlayUnit); + } +#endif +} + +- (void)error:(OSStatus)error position:(NSString *)position { + if (error != noErr) { + NSString *errorInfo = [NSString stringWithFormat:@" Error: %d, Position: %@", (int)error, position]; + if ([self.delegate respondsToSelector:@selector(audioController:error:info:)]) { + [self.delegate audioController:self error:error info:position]; + } + NSLog(@" :%@", errorInfo); + } +} + +- (AudioStreamBasicDescription)signedIntegerStreamFormatDesc { + AudioStreamBasicDescription streamFormatDesc; + streamFormatDesc.mSampleRate = _sampleRate; + streamFormatDesc.mFormatID = kAudioFormatLinearPCM; + streamFormatDesc.mFormatFlags = (kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked); + streamFormatDesc.mChannelsPerFrame = _channelCount; + streamFormatDesc.mFramesPerPacket = 1; + streamFormatDesc.mBitsPerChannel = 16; + streamFormatDesc.mBytesPerFrame = streamFormatDesc.mBitsPerChannel / 8 * streamFormatDesc.mChannelsPerFrame; + streamFormatDesc.mBytesPerPacket = streamFormatDesc.mBytesPerFrame * streamFormatDesc.mFramesPerPacket; + + return streamFormatDesc; +} + +- (void)dealloc { + if (_remoteIOUnit) { + AudioOutputUnitStop(_remoteIOUnit); + AudioComponentInstanceDispose(_remoteIOUnit); + _remoteIOUnit = nil; + } + +#if !TARGET_OS_IPHONE + if (_macPlayUnit) { + AudioOutputUnitStop(_macPlayUnit); + AudioComponentInstanceDispose(_macPlayUnit); + _macPlayUnit = nil; + } +#endif + + NSLog(@" AudioController dealloc"); +} + +@end diff --git a/macOS/APIExample/Commons/ExternalAudio/AudioOptions.h b/macOS/APIExample/Commons/ExternalAudio/AudioOptions.h new file mode 100644 index 000000000..0a40ef9cc --- /dev/null +++ b/macOS/APIExample/Commons/ExternalAudio/AudioOptions.h @@ -0,0 +1,40 @@ +// +// AudioOptions.h +// AgoraAudioIO +// +// Created by CavanSu on 12/03/2018. +// Copyright © 2018 CavanSu. All rights reserved. +// + +#ifndef AudioOptions_h +#define AudioOptions_h + +typedef NS_ENUM(int, AudioCRMode) { + AudioCRModeExterCaptureSDKRender = 1, + AudioCRModeSDKCaptureExterRender = 2, + AudioCRModeSDKCaptureSDKRender = 3, + AudioCRModeExterCaptureExterRender = 4 +}; + +typedef NS_ENUM(int, IOUnitType) { + IOUnitTypeVPIO, + IOUnitTypeRemoteIO +}; + +typedef NS_ENUM(int, ChannelMode) { + ChannelModeCommunication = 0, + ChannelModeLiveBroadcast = 1 +}; + +typedef NS_ENUM(int, ClientRole) { + ClientRoleAudience = 0, + ClientRoleBroadcast = 1 +}; + +#if TARGET_OS_IPHONE +#import "UIColor+CSRGB.h" +#import "UIView+CSshortFrame.h" +#define ThemeColor [UIColor Red: 122 Green: 203 Blue: 253] +#endif + +#endif /* AudioOptions_h */ diff --git a/macOS/APIExample/Commons/ExternalAudio/AudioWriteToFile.h b/macOS/APIExample/Commons/ExternalAudio/AudioWriteToFile.h new file mode 100644 index 000000000..9ccf24b14 --- /dev/null +++ b/macOS/APIExample/Commons/ExternalAudio/AudioWriteToFile.h @@ -0,0 +1,13 @@ +// +// AudioWriteToFile.h +// AudioCapture +// +// Created by CavanSu on 08/11/2017. +// Copyright © 2017 Agora. All rights reserved. +// + +#import + +@interface AudioWriteToFile : NSObject ++ (void)writeToFileWithData:(void *)data length:(int)bytes; +@end diff --git a/macOS/APIExample/Commons/ExternalAudio/AudioWriteToFile.m b/macOS/APIExample/Commons/ExternalAudio/AudioWriteToFile.m new file mode 100644 index 000000000..54558635a --- /dev/null +++ b/macOS/APIExample/Commons/ExternalAudio/AudioWriteToFile.m @@ -0,0 +1,39 @@ +// +// AudioWriteToFile.m +// AudioCapture +// +// Created by CavanSu on 08/11/2017. +// Copyright © 2017 Agora. All rights reserved. +// + +#import "AudioWriteToFile.h" + +@implementation AudioWriteToFile + +static NSFileHandle *file = nil; +static dispatch_queue_t queue = nil; + ++ (void)load { + queue = dispatch_queue_create("writeFile", NULL); +} + ++ (void)writeToFileWithData:(void *)data length:(int)bytes { + if(NULL == data || bytes < 1) return; + + dispatch_async(queue, ^{ + + if (file == nil) { + NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0] stringByAppendingPathComponent:@"1.pcm"]; + [[NSFileManager defaultManager] removeItemAtPath:path error:nil]; + if (![[NSFileManager defaultManager] createFileAtPath:path contents:nil attributes:nil]) { + + } + else { + file = [NSFileHandle fileHandleForWritingAtPath:path]; + } + } + [file writeData:[NSData dataWithBytes:data length:bytes]]; + }); +} + +@end diff --git a/macOS/APIExample/Commons/ExternalAudio/ExternalAudio.h b/macOS/APIExample/Commons/ExternalAudio/ExternalAudio.h new file mode 100644 index 000000000..17e1cb3a1 --- /dev/null +++ b/macOS/APIExample/Commons/ExternalAudio/ExternalAudio.h @@ -0,0 +1,26 @@ +// +// ExternalAudio.h +// AgoraAudioIO +// +// Created by CavanSu on 22/01/2018. +// Copyright © 2018 CavanSu. All rights reserved. +// + +#import +#import "AudioOptions.h" + +@class AgoraRtcEngineKit; +@class ExternalAudio; +@protocol ExternalAudioDelegate +@optional +- (void)externalAudio:(ExternalAudio *)externalAudio errorInfo:(NSString *)errorInfo; +@end + +@interface ExternalAudio : NSObject +@property (nonatomic, weak) id delegate; + ++ (instancetype)sharedExternalAudio; +- (void)setupExternalAudioWithAgoraKit:(AgoraRtcEngineKit *)agoraKit sampleRate:(uint)sampleRate channels:(uint)channels audioCRMode:(AudioCRMode)audioCRMode IOType:(IOUnitType)ioType; +- (void)startWork; +- (void)stopWork; +@end diff --git a/macOS/APIExample/Commons/ExternalAudio/ExternalAudio.mm b/macOS/APIExample/Commons/ExternalAudio/ExternalAudio.mm new file mode 100644 index 000000000..04bae4402 --- /dev/null +++ b/macOS/APIExample/Commons/ExternalAudio/ExternalAudio.mm @@ -0,0 +1,310 @@ +// +// ExternalAudio.m +// AgoraAudioIO +// +// Created by CavanSu on 22/01/2018. +// Copyright © 2018 CavanSu. All rights reserved. +// + +#import "ExternalAudio.h" +#import "AudioController.h" +#import "AudioWriteToFile.h" + +#if TARGET_OS_IPHONE +#import +#import +#import +#else +#import +#import +#import +#endif + +@interface ExternalAudio () +@property (nonatomic, strong) AudioController *audioController; +@property (nonatomic, assign) AudioCRMode audioCRMode; +@property (nonatomic, assign) int sampleRate; +@property (nonatomic, assign) int channelCount; +@property (nonatomic, weak) AgoraRtcEngineKit *agoraKit; +@end + +@implementation ExternalAudio + +static NSObject *threadLockCapture; +static NSObject *threadLockPlay; + +#pragma mark - C++ ExternalAudioFrameObserver +class ExternalAudioFrameObserver : public agora::media::IAudioFrameObserver +{ +private: + + // total buffer length of per second + enum { kBufferLengthBytes = 441 * 2 * 2 * 50 }; // + + // capture + char byteBuffer[kBufferLengthBytes]; // char take up 1 byte, byterBuffer[] take up 88200 bytes + int readIndex = 0; + int writeIndex = 0; + int availableBytes = 0; + int channels = 1; + + // play + char byteBuffer_play[kBufferLengthBytes]; + int readIndex_play = 0; + int writeIndex_play = 0; + int availableBytes_play = 0; + int channels_play = 1; + +public: + int sampleRate = 0; + int sampleRate_play = 0; + + bool isExternalCapture = false; + bool isExternalRender = false; + +#pragma mark- + // push audio data to special buffer(Array byteBuffer) + // bytesLength = date length + void pushExternalData(void* data, int bytesLength) + { + @synchronized(threadLockCapture) { + + if (availableBytes + bytesLength > kBufferLengthBytes) { + + readIndex = 0; + writeIndex = 0; + availableBytes = 0; + } + + if (writeIndex + bytesLength > kBufferLengthBytes) { + + int left = kBufferLengthBytes - writeIndex; + memcpy(byteBuffer + writeIndex, data, left); + memcpy(byteBuffer, (char *)data + left, bytesLength - left); + writeIndex = bytesLength - left; + } + else { + + memcpy(byteBuffer + writeIndex, data, bytesLength); + writeIndex += bytesLength; + } + availableBytes += bytesLength; + } + + } + + // copy byteBuffer to audioFrame.buffer + virtual bool onRecordAudioFrame(AudioFrame& audioFrame) override + { + @synchronized(threadLockCapture) { + + if (isExternalCapture == false) return true; + + int readBytes = sampleRate / 100 * channels * audioFrame.bytesPerSample; + + if (availableBytes < readBytes) { + return false; + } + + audioFrame.samplesPerSec = sampleRate; + unsigned char tmp[960]; // The most rate:@48k fs, channels = 1, the most total size = 960; + + if (readIndex + readBytes > kBufferLengthBytes) { + int left = kBufferLengthBytes - readIndex; + memcpy(tmp, byteBuffer + readIndex, left); + memcpy(tmp + left, byteBuffer, readBytes - left); + readIndex = readBytes - left; + } + else { + memcpy(tmp, byteBuffer + readIndex, readBytes); + readIndex += readBytes; + } + + availableBytes -= readBytes; + + if (channels == audioFrame.channels) { + memcpy(audioFrame.buffer, tmp, readBytes); + } + [AudioWriteToFile writeToFileWithData:audioFrame.buffer length:readBytes]; + return true; + } + + } + +#pragma mark- + // read Audio data from byteBuffer_play to audioUnit + int readAudioData(void* data, int bytesLength) + { + @synchronized(threadLockPlay) { + + if (NULL == data || bytesLength < 1 || availableBytes_play < bytesLength) { + return 0; + } + + int readBytes = bytesLength; + + unsigned char tmp[4096]; // unsigned char takes up 1 byte + + if (readIndex_play + readBytes > kBufferLengthBytes) { + + int left = kBufferLengthBytes - readIndex_play; + memcpy(tmp, byteBuffer_play + readIndex_play, left); + memcpy(tmp + left, byteBuffer_play, readBytes - left); + readIndex_play = readBytes - left; + } + else { + + memcpy(tmp, byteBuffer_play + readIndex_play, readBytes); + readIndex_play += readBytes; + } + + availableBytes_play -= readBytes; + + if (channels_play == 1) { + memcpy(data, tmp, readBytes); + } + + [AudioWriteToFile writeToFileWithData:data length:readBytes]; + + return readBytes; + } + + } + + // recive remote audio stream, push audio data to byteBuffer_play + virtual bool onPlaybackAudioFrame(AudioFrame& audioFrame) override + { + @synchronized(threadLockPlay) { + + if (isExternalRender == false) return true; + + int bytesLength = audioFrame.samples * audioFrame.channels * audioFrame.bytesPerSample; + char *data = (char *)audioFrame.buffer; + + sampleRate_play = audioFrame.samplesPerSec; + channels_play = audioFrame.channels; + + if (availableBytes_play + bytesLength > kBufferLengthBytes) { + + readIndex_play = 0; + writeIndex_play = 0; + availableBytes_play = 0; + } + + if (writeIndex_play + bytesLength > kBufferLengthBytes) { + + int left = kBufferLengthBytes - writeIndex_play; + memcpy(byteBuffer_play + writeIndex_play, data, left); + memcpy(byteBuffer_play, (char *)data + left, bytesLength - left); + writeIndex_play = bytesLength - left; + } + else { + + memcpy(byteBuffer_play + writeIndex_play, data, bytesLength); + writeIndex_play += bytesLength; + } + + availableBytes_play += bytesLength; + + return true; + } + + } + + virtual bool onPlaybackAudioFrameBeforeMixing(unsigned int uid, AudioFrame& audioFrame) override { return true; } + + virtual bool onMixedAudioFrame(AudioFrame& audioFrame) override { return true; } +}; + +static ExternalAudioFrameObserver* s_audioFrameObserver; + + ++ (instancetype)sharedExternalAudio { + ExternalAudio *audio = [[ExternalAudio alloc] init]; + return audio; +} + +- (void)setupExternalAudioWithAgoraKit:(AgoraRtcEngineKit *)agoraKit sampleRate:(uint)sampleRate channels:(uint)channels audioCRMode:(AudioCRMode)audioCRMode IOType:(IOUnitType)ioType { + + threadLockCapture = [[NSObject alloc] init]; + threadLockPlay = [[NSObject alloc] init]; + + // AudioController + self.audioController = [AudioController audioController]; + self.audioController.delegate = self; + [self.audioController setUpAudioSessionWithSampleRate:sampleRate channelCount:channels audioCRMode:audioCRMode IOType:ioType]; + + // Agora Engine of C++ + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)agoraKit.getNativeHandle; + agora::util::AutoPtr mediaEngine; + mediaEngine.queryInterface(rtc_engine, agora::AGORA_IID_MEDIA_ENGINE); + + if (mediaEngine) { + s_audioFrameObserver = new ExternalAudioFrameObserver(); + s_audioFrameObserver -> sampleRate = sampleRate; + s_audioFrameObserver -> sampleRate_play = channels; + mediaEngine->registerAudioFrameObserver(s_audioFrameObserver); + } + + if (audioCRMode == AudioCRModeExterCaptureExterRender || audioCRMode == AudioCRModeSDKCaptureExterRender) { + s_audioFrameObserver -> isExternalRender = true; + } + if (audioCRMode == AudioCRModeExterCaptureExterRender || audioCRMode == AudioCRModeExterCaptureSDKRender) { + s_audioFrameObserver -> isExternalCapture = true; + } + + self.agoraKit = agoraKit; + self.audioCRMode = audioCRMode; +} + +- (void)startWork { + [self.audioController startWork]; +} + +- (void)stopWork { + [self.audioController stopWork]; + [self cancelRegiset]; +} + +- (void)cancelRegiset { + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)self.agoraKit.getNativeHandle; + agora::util::AutoPtr mediaEngine; + mediaEngine.queryInterface(rtc_engine, agora::AGORA_IID_MEDIA_ENGINE); + mediaEngine->registerAudioFrameObserver(NULL); +} + +- (void)audioController:(AudioController *)controller didCaptureData:(unsigned char *)data bytesLength:(int)bytesLength { + + if (self.audioCRMode != AudioCRModeExterCaptureSDKRender) { + if (s_audioFrameObserver) { + s_audioFrameObserver -> pushExternalData(data, bytesLength); + } + } + else { + [self.agoraKit pushExternalAudioFrameRawData:data samples:bytesLength / 2 timestamp:0]; + } + +} + +- (int)audioController:(AudioController *)controller didRenderData:(unsigned char *)data bytesLength:(int)bytesLength { + int result = 0; + + if (s_audioFrameObserver) { + result = s_audioFrameObserver -> readAudioData(data, bytesLength); + } + + return result; +} + +- (void)audioController:(AudioController *)controller error:(OSStatus)error info:(NSString *)info { + if ([self.delegate respondsToSelector:@selector(externalAudio:errorInfo:)]) { + NSString *errorInfo = [NSString stringWithFormat:@" error:%d, info:%@", error, info]; + [self.delegate externalAudio:self errorInfo:errorInfo]; + } +} + +- (void)dealloc { + NSLog(@"ExAudio dealloc"); +} + +@end diff --git a/macOS/APIExample/Commons/ExternalVideo/AgoraCameraSourceMediaIO.swift b/macOS/APIExample/Commons/ExternalVideo/AgoraCameraSourceMediaIO.swift new file mode 100644 index 000000000..bf53be100 --- /dev/null +++ b/macOS/APIExample/Commons/ExternalVideo/AgoraCameraSourceMediaIO.swift @@ -0,0 +1,171 @@ +// +// AgoraCamera.swift +// Agora-Custom-Media-Device +// +// Created by GongYuhua on 2017/11/10. +// Copyright © 2017年 Agora.io All rights reserved. +// + + +import Cocoa +import AgoraRtcKit + +extension AVCaptureDevice.Position { + func reverse() -> AVCaptureDevice.Position { + switch self { + case .front: return .back + case .back, .unspecified: return .front + default: return .front + } + } + + func isFront() -> Bool { + return self == .front + } +} + +class AgoraCameraSourceMediaIO: NSObject { + var consumer: AgoraVideoFrameConsumer? + + var isFront: Bool { + get { + return position.isFront() + } + } + + private var position = AVCaptureDevice.Position.front + private var captureSession: AVCaptureSession? + private var captureQueue: DispatchQueue? + private var currentOutput: AVCaptureVideoDataOutput? { + if let outputs = self.captureSession?.outputs as? [AVCaptureVideoDataOutput] { + return outputs.first + } else { + return nil + } + } +} + +private extension AgoraCameraSourceMediaIO { + func initialize() -> Bool { + let captureSession = AVCaptureSession() + let captureOutput = AVCaptureVideoDataOutput() + captureOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange] + if captureSession.canAddOutput(captureOutput) { + captureSession.addOutput(captureOutput) + } + + self.captureSession = captureSession + captureQueue = DispatchQueue(label: "Agora-Custom-Video-Capture-Queue") + + return true + } + + func startCapture() { + guard let currentOutput = currentOutput, let captureQueue = captureQueue else { + return + } + + currentOutput.setSampleBufferDelegate(self, queue: captureQueue) + captureQueue.async { [weak self] in + guard let strongSelf = self, let captureSession = strongSelf.captureSession else { + return + } + strongSelf.changeCaptureDevice(toPosition: strongSelf.position, ofSession: captureSession) + captureSession.beginConfiguration() + if captureSession.canSetSessionPreset(.vga640x480) { + captureSession.sessionPreset = .vga640x480 + } + captureSession.commitConfiguration() + captureSession.startRunning() + } + } + + func stopCapture() { + currentOutput?.setSampleBufferDelegate(nil, queue: nil) + captureQueue?.async { [weak self] in + self?.captureSession?.stopRunning() + } + } + + func dispose() { + captureQueue = nil + captureSession = nil + } +} + +private extension AgoraCameraSourceMediaIO { + func changeCaptureDevice(toPosition position: AVCaptureDevice.Position, ofSession captureSession: AVCaptureSession) { + guard let captureDevice = captureDevice(atPosition: position) else { + return + } + + let currentInputs = captureSession.inputs as? [AVCaptureDeviceInput] + let currentInput = currentInputs?.first + + if let currentInput = currentInput, currentInput.device.localizedName == captureDevice.uniqueID { + return + } + + guard let newInput = try? AVCaptureDeviceInput(device: captureDevice) else { + return + } + + captureSession.beginConfiguration() + if let currentInput = currentInput { + captureSession.removeInput(currentInput) + } + if captureSession.canAddInput(newInput) { + captureSession.addInput(newInput) + } + captureSession.commitConfiguration() + } + + func captureDevice(atPosition position: AVCaptureDevice.Position) -> AVCaptureDevice? { + let devices = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: .video, position: position).devices + return devices.first + } +} + +extension AgoraCameraSourceMediaIO: AgoraVideoSourceProtocol { + func shouldInitialize() -> Bool { + return initialize() + } + + func shouldStart() { + startCapture() + } + + func shouldStop() { + stopCapture() + } + + func shouldDispose() { + dispose() + } + + func bufferType() -> AgoraVideoBufferType { + return .pixelBuffer + } + + func contentHint() -> AgoraVideoContentHint { + return .none + } + + func captureType() -> AgoraVideoCaptureType { + return .camera + } +} + +extension AgoraCameraSourceMediaIO: AVCaptureVideoDataOutputSampleBufferDelegate { + func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { + guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer), CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) == kCVReturnSuccess else { + return + } + defer { + CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) + } + + let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + consumer?.consumePixelBuffer(pixelBuffer, withTimestamp: time, rotation: .rotationNone) + } +} diff --git a/macOS/APIExample/Commons/ExternalVideo/AgoraCameraSourcePush.swift b/macOS/APIExample/Commons/ExternalVideo/AgoraCameraSourcePush.swift new file mode 100644 index 000000000..9fd339815 --- /dev/null +++ b/macOS/APIExample/Commons/ExternalVideo/AgoraCameraSourcePush.swift @@ -0,0 +1,187 @@ +// +// MyVideoCapture.swift +// Agora-Video-Source +// +// Created by GongYuhua on 2017/4/11. +// Copyright © 2017年 Agora. All rights reserved. +// + +import Cocoa +import AVFoundation + +class CustomVideoSourcePreview : VideoView { + private var previewLayer: AVCaptureVideoPreviewLayer? + + func insertCaptureVideoPreviewLayer(previewLayer: AVCaptureVideoPreviewLayer) { + self.previewLayer?.removeFromSuperlayer() + + previewLayer.frame = bounds + if let layer = self.layer { + layer.insertSublayer(previewLayer, below: layer.sublayers?.first) + } + self.previewLayer = previewLayer + } + + override func layout() { + super.layout() + previewLayer?.frame = bounds + } +} + +protocol AgoraCameraSourcePushDelegate { + func myVideoCapture(_ capture: AgoraCameraSourcePush, didOutputSampleBuffer pixelBuffer: CVPixelBuffer, rotation: Int, timeStamp: CMTime) +} + +enum Camera: Int { + case front = 1 + case back = 0 + + static func defaultCamera() -> Camera { + return .front + } + + func next() -> Camera { + switch self { + case .back: return .front + case .front: return .back + } + } +} + +class AgoraCameraSourcePush: NSObject { + + fileprivate var delegate: AgoraCameraSourcePushDelegate? + private var videoView: CustomVideoSourcePreview + + private var currentCamera = Camera.defaultCamera() + private let captureSession: AVCaptureSession + private let captureQueue: DispatchQueue + private var currentOutput: AVCaptureVideoDataOutput? { + if let outputs = self.captureSession.outputs as? [AVCaptureVideoDataOutput] { + return outputs.first + } else { + return nil + } + } + + init(delegate: AgoraCameraSourcePushDelegate?, videoView: CustomVideoSourcePreview) { + self.delegate = delegate + self.videoView = videoView + + captureSession = AVCaptureSession() + + let captureOutput = AVCaptureVideoDataOutput() + captureOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange] + if captureSession.canAddOutput(captureOutput) { + captureSession.addOutput(captureOutput) + } + + captureQueue = DispatchQueue(label: "MyCaptureQueue") + + let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + videoView.insertCaptureVideoPreviewLayer(previewLayer: previewLayer) + } + + deinit { + captureSession.stopRunning() + } + + func startCapture(ofCamera camera: Camera) { + guard let currentOutput = currentOutput else { + return + } + + currentCamera = camera + currentOutput.setSampleBufferDelegate(self, queue: captureQueue) + + captureQueue.async { [weak self] in + guard let strongSelf = self else { + return + } + strongSelf.changeCaptureDevice(toIndex: camera.rawValue, ofSession: strongSelf.captureSession) + strongSelf.captureSession.beginConfiguration() + if strongSelf.captureSession.canSetSessionPreset(AVCaptureSession.Preset.vga640x480) { + strongSelf.captureSession.sessionPreset = AVCaptureSession.Preset.vga640x480 + } + strongSelf.captureSession.commitConfiguration() + strongSelf.captureSession.startRunning() + } + } + + func stopCapture() { + currentOutput?.setSampleBufferDelegate(nil, queue: nil) + captureQueue.async { [weak self] in + self?.captureSession.stopRunning() + } + } + + func switchCamera() { + stopCapture() + currentCamera = currentCamera.next() + startCapture(ofCamera: currentCamera) + } +} + +private extension AgoraCameraSourcePush { + func changeCaptureDevice(toIndex index: Int, ofSession captureSession: AVCaptureSession) { + guard let captureDevice = captureDevice(atIndex: index) else { + return + } + + let currentInputs = captureSession.inputs as? [AVCaptureDeviceInput] + let currentInput = currentInputs?.first + + if let currentInputName = currentInput?.device.localizedName, + currentInputName == captureDevice.uniqueID { + return + } + + guard let newInput = try? AVCaptureDeviceInput(device: captureDevice) else { + return + } + + captureSession.beginConfiguration() + if let currentInput = currentInput { + captureSession.removeInput(currentInput) + } + if captureSession.canAddInput(newInput) { + captureSession.addInput(newInput) + } + captureSession.commitConfiguration() + } + + func captureDevice(atIndex index: Int) -> AVCaptureDevice? { + let deviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: AVMediaType.video, position: .back) + let devices = deviceDiscoverySession.devices + + let count = devices.count + guard count > 0, index >= 0 else { + return nil + } + + let device: AVCaptureDevice + if index >= count { + device = devices.last! + } else { + device = devices[index] + } + + return device + } +} + +extension AgoraCameraSourcePush: AVCaptureVideoDataOutputSampleBufferDelegate { + func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { + guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { + return + } + let time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) + DispatchQueue.main.async {[weak self] in + guard let weakSelf = self else { + return + } + + weakSelf.delegate?.myVideoCapture(weakSelf, didOutputSampleBuffer: pixelBuffer, rotation: 90, timeStamp: time) + } + } +} diff --git a/macOS/APIExample/Commons/ExternalVideo/AgoraMetalRender.swift b/macOS/APIExample/Commons/ExternalVideo/AgoraMetalRender.swift new file mode 100644 index 000000000..d3212c081 --- /dev/null +++ b/macOS/APIExample/Commons/ExternalVideo/AgoraMetalRender.swift @@ -0,0 +1,299 @@ +// +// AgoraMetalRender.swift +// Agora-Custom-Media-Device +// +// Created by GongYuhua on 2017/11/15. +// Copyright © 2017年 Agora.io All rights reserved. +// + +import CoreMedia +import Metal +#if os(macOS) || (os(iOS) && (!arch(i386) && !arch(x86_64))) + import MetalKit +#endif +import AgoraRtcKit + +protocol AgoraMetalRenderMirrorDataSource: NSObjectProtocol { + func renderViewShouldMirror(renderView: AgoraMetalRender) -> Bool +} + +class AgoraMetalRender: NSView { + weak var mirrorDataSource: AgoraMetalRenderMirrorDataSource? + + fileprivate var textures: [MTLTexture]? + fileprivate var vertexBuffer: MTLBuffer? + fileprivate var viewSize = CGSize.zero + + fileprivate var device = MTLCreateSystemDefaultDevice() + fileprivate var renderPipelineState: MTLRenderPipelineState? + fileprivate let semaphore = DispatchSemaphore(value: 1) + fileprivate var metalDevice = MTLCreateSystemDefaultDevice() +#if os(macOS) || (os(iOS) && (!arch(i386) && !arch(x86_64))) + fileprivate var metalView: MTKView! + fileprivate var textureCache: CVMetalTextureCache? +#endif + fileprivate var commandQueue: MTLCommandQueue? + + init() { + super.init(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + initializeMetalView() + initializeTextureCache() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + initializeMetalView() + initializeTextureCache() + } + + override init(frame frameRect: CGRect) { + super.init(frame: frameRect) + initializeMetalView() + initializeTextureCache() + } + + + override func layout() { + super.layout() + viewSize = bounds.size + } +} + +extension AgoraMetalRender: AgoraVideoSinkProtocol { + func shouldInitialize() -> Bool { + initializeRenderPipelineState() + return true + } + + func shouldStart() { + #if os(macOS) || (os(iOS) && (!arch(i386) && !arch(x86_64))) + metalView.delegate = self + #endif + } + + func shouldStop() { + #if os(macOS) || (os(iOS) && (!arch(i386) && !arch(x86_64))) + metalView.delegate = nil + #endif + } + + func shouldDispose() { + textures = nil + } + + func bufferType() -> AgoraVideoBufferType { + return .pixelBuffer + } + + func pixelFormat() -> AgoraVideoPixelFormat { + return .NV12 + } + + func renderPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: AgoraVideoRotation) { + #if os(macOS) || (os(iOS) && (!arch(i386) && !arch(x86_64))) + guard CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) == kCVReturnSuccess else { + return + } + defer { + CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) + } + + let isPlanar = CVPixelBufferIsPlanar(pixelBuffer) + let width = isPlanar ? CVPixelBufferGetWidthOfPlane(pixelBuffer, 0) : CVPixelBufferGetWidth(pixelBuffer) + let height = isPlanar ? CVPixelBufferGetHeightOfPlane(pixelBuffer, 0) : CVPixelBufferGetHeight(pixelBuffer) + let size = CGSize(width: width, height: height) + + let mirror = mirrorDataSource?.renderViewShouldMirror(renderView: self) ?? false + if let renderedCoordinates = rotation.renderedCoordinates(mirror: mirror, + videoSize: size, + viewSize: viewSize) { + let byteLength = 16 * MemoryLayout.size(ofValue: renderedCoordinates[0]) + vertexBuffer = device?.makeBuffer(bytes: renderedCoordinates, length: byteLength, options: []) + } + + if let yTexture = texture(pixelBuffer: pixelBuffer, textureCache: textureCache, planeIndex: 0, pixelFormat: .r8Unorm), + let uvTexture = texture(pixelBuffer: pixelBuffer, textureCache: textureCache, planeIndex: 1, pixelFormat: .rg8Unorm) { + self.textures = [yTexture, uvTexture] + } + #endif + } +} + +private extension AgoraMetalRender { + func initializeMetalView() { + #if os(macOS) || (os(iOS) && (!arch(i386) && !arch(x86_64))) + metalView = MTKView(frame: bounds, device: device) + metalView.framebufferOnly = true + metalView.colorPixelFormat = .bgra8Unorm + metalView.autoresizingMask = [.width, .height] + addSubview(metalView) + commandQueue = device?.makeCommandQueue() + #endif + } + + func initializeRenderPipelineState() { + guard let device = device, let library = device.makeDefaultLibrary() else { + return + } + + let pipelineDescriptor = MTLRenderPipelineDescriptor() + pipelineDescriptor.sampleCount = 1 + pipelineDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm + pipelineDescriptor.depthAttachmentPixelFormat = .invalid + + pipelineDescriptor.vertexFunction = library.makeFunction(name: "mapTexture") + pipelineDescriptor.fragmentFunction = library.makeFunction(name: "displayNV12Texture") + + renderPipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDescriptor) + } + + func initializeTextureCache() { + #if os(macOS) || (os(iOS) && (!arch(i386) && !arch(x86_64))) + guard let metalDevice = metalDevice, + CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, metalDevice, nil, &textureCache) == kCVReturnSuccess else { + return + } + #endif + } + +#if os(macOS) || (os(iOS) && (!arch(i386) && !arch(x86_64))) + func texture(pixelBuffer: CVPixelBuffer, textureCache: CVMetalTextureCache?, planeIndex: Int = 0, pixelFormat: MTLPixelFormat = .bgra8Unorm) -> MTLTexture? { + guard let textureCache = textureCache, CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) == kCVReturnSuccess else { + return nil + } + defer { + CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) + } + + let isPlanar = CVPixelBufferIsPlanar(pixelBuffer) + let width = isPlanar ? CVPixelBufferGetWidthOfPlane(pixelBuffer, planeIndex) : CVPixelBufferGetWidth(pixelBuffer) + let height = isPlanar ? CVPixelBufferGetHeightOfPlane(pixelBuffer, planeIndex) : CVPixelBufferGetHeight(pixelBuffer) + + var imageTexture: CVMetalTexture? + let result = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, textureCache, pixelBuffer, nil, pixelFormat, width, height, planeIndex, &imageTexture) + + guard let unwrappedImageTexture = imageTexture, + let texture = CVMetalTextureGetTexture(unwrappedImageTexture), + result == kCVReturnSuccess + else { + return nil + } + + return texture + } +#endif +} + +#if os(macOS) || (os(iOS) && (!arch(i386) && !arch(x86_64))) +extension AgoraMetalRender: MTKViewDelegate { + public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { + + } + + public func draw(in: MTKView) { + guard viewSize.width > 0 && viewSize.height > 0 else { + return + } + + _ = semaphore.wait(timeout: .distantFuture) + autoreleasepool { + guard let textures = textures, let device = device, + let commandBuffer = commandQueue?.makeCommandBuffer() else { + _ = semaphore.signal() + return + } + + render(textures: textures, withCommandBuffer: commandBuffer, device: device) + } + } + + private func render(textures: [MTLTexture], withCommandBuffer commandBuffer: MTLCommandBuffer, device: MTLDevice) { + guard let currentRenderPassDescriptor = metalView.currentRenderPassDescriptor, + let currentDrawable = metalView.currentDrawable, + let renderPipelineState = renderPipelineState, + let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: currentRenderPassDescriptor) else { + semaphore.signal() + return + } + + encoder.pushDebugGroup("Agora-Custom-Render-Frame") + encoder.setRenderPipelineState(renderPipelineState) + encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0) + + if let textureY = textures.first, let textureUV = textures.last { + encoder.setFragmentTexture(textureY, index: 0) + encoder.setFragmentTexture(textureUV, index: 1) + encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) + } + + encoder.popDebugGroup() + encoder.endEncoding() + + commandBuffer.addScheduledHandler { [weak self] (buffer) in + self?.semaphore.signal() + } + commandBuffer.present(currentDrawable) + commandBuffer.commit() + } +} +#endif + +extension AgoraVideoRotation { + func renderedCoordinates(mirror: Bool, videoSize: CGSize, viewSize: CGSize) -> [float4]? { + guard viewSize.width > 0, viewSize.height > 0, videoSize.width > 0, videoSize.height > 0 else { + return nil + } + + let widthAspito: Float + let heightAspito: Float + if self == .rotation90 || self == .rotation270 { + widthAspito = Float(videoSize.height / viewSize.width) + heightAspito = Float(videoSize.width / viewSize.height) + } else { + widthAspito = Float(videoSize.width / viewSize.width) + heightAspito = Float(videoSize.height / viewSize.height) + } + + let x: Float + let y: Float + if widthAspito < heightAspito { + x = 1 + y = heightAspito / widthAspito + } else { + x = widthAspito / heightAspito + y = 1 + } + + let A = float4( x, -y, 0.0, 1.0 ) + let B = float4( -x, -y, 0.0, 1.0 ) + let C = float4( x, y, 0.0, 1.0 ) + let D = float4( -x, y, 0.0, 1.0 ) + + switch self { + case .rotationNone: + if mirror { + return [A, B, C, D] + } else { + return [B, A, D, C] + } + case .rotation90: + if mirror { + return [C, A, D, B] + } else { + return [D, B, C, A] + } + case .rotation180: + if mirror { + return [D, C, B, A] + } else { + return [C, D, A, B] + } + case .rotation270: + if mirror { + return [B, D, A, C] + } else { + return [A, C, B, D] + } + } + } +} diff --git a/macOS/APIExample/Commons/ExternalVideo/AgoraMetalShader.metal b/macOS/APIExample/Commons/ExternalVideo/AgoraMetalShader.metal new file mode 100644 index 000000000..f324b228f --- /dev/null +++ b/macOS/APIExample/Commons/ExternalVideo/AgoraMetalShader.metal @@ -0,0 +1,49 @@ +// +// AgoraMetalShader.metal +// Agora-Custom-Media-Device +// +// Created by GongYuhua on 2017/11/15. +// Copyright © 2017年 Agora. All rights reserved. +// + +#include + +using namespace metal; + +typedef struct { + float4 renderedCoordinate [[position]]; + float2 textureCoordinate; +} TextureMappingVertex; + +vertex TextureMappingVertex mapTexture(unsigned int vertex_id [[ vertex_id ]], + const device packed_float4* vertex_array [[ buffer(0) ]]) { + + float4x4 renderedCoordinates = float4x4(vertex_array[0], vertex_array[1], vertex_array[2], vertex_array[3]); + float4x2 textureCoordinates = float4x2(float2( 0.0, 1.0 ), + float2( 1.0, 1.0 ), + float2( 0.0, 0.0 ), + float2( 1.0, 0.0 )); + + TextureMappingVertex outVertex; + outVertex.renderedCoordinate = renderedCoordinates[vertex_id]; + outVertex.textureCoordinate = textureCoordinates[vertex_id]; + + return outVertex; +} + +fragment float4 displayNV12Texture(TextureMappingVertex mappingVertex [[stage_in]], + texture2d textureY [[ texture(0) ]], + texture2d textureUV [[ texture(1) ]]) { + constexpr sampler colorSampler(mip_filter::linear, + mag_filter::linear, + min_filter::linear); + + const float4x4 ycbcrToRGBTransform = float4x4(float4(+1.0000f, +1.0000f, +1.0000f, +0.0000f), + float4(+0.0000f, -0.3441f, +1.7720f, +0.0000f), + float4(+1.4020f, -0.7141f, +0.0000f, +0.0000f), + float4(-0.7010f, +0.5291f, -0.8860f, +1.0000f)); + + float4 ycbcr = float4(textureY.sample(colorSampler, mappingVertex.textureCoordinate).r, + textureUV.sample(colorSampler, mappingVertex.textureCoordinate).rg, 1.0); + return ycbcrToRGBTransform * ycbcr; +} diff --git a/macOS/APIExample/Commons/GlobalSettings.swift b/macOS/APIExample/Commons/GlobalSettings.swift new file mode 100644 index 000000000..0fa004bc9 --- /dev/null +++ b/macOS/APIExample/Commons/GlobalSettings.swift @@ -0,0 +1,49 @@ +// +// GlobalSettings.swift +// APIExample +// +// Created by 张乾泽 on 2020/9/25. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import Foundation +import AgoraRtcKit + +struct SettingItemOption { + var idx: Int + var label: String + var value: T +} + +class SettingItem { + var selected: Int + var options: [SettingItemOption] + + func selectedOption() -> SettingItemOption { + return options[selected] + } + + init(selected: Int, options: [SettingItemOption]) { + self.selected = selected + self.options = options + } +} + +class GlobalSettings { + // The region for connection. This advanced feature applies to scenarios that have regional restrictions. + // For the regions that Agora supports, see https://docs.agora.io/en/Interactive%20Broadcast/API%20Reference/oc/Constants/AgoraAreaCode.html. After specifying the region, the SDK connects to the Agora servers within that region. + var area:AgoraAreaCode = .GLOB + static let shared = GlobalSettings() + let resolutionSetting: SettingItem = SettingItem( + selected: Configs.defaultResolutionIdx, + options: Configs.Resolutions.enumerated().map { + SettingItemOption(idx: $0.offset, label: $0.element.name(), value: $0.offset) + } + ) + let fpsSetting: SettingItem = SettingItem( + selected: Configs.defaultFpsIdx, + options: Configs.Fps.enumerated().map { + SettingItemOption(idx: $0.offset, label: "\($0.element)fps", value: $0.offset) + } + ) +} diff --git a/macOS/APIExample/Commons/KeyCenter.swift b/macOS/APIExample/Commons/KeyCenter.swift new file mode 100644 index 000000000..0de8c2ad3 --- /dev/null +++ b/macOS/APIExample/Commons/KeyCenter.swift @@ -0,0 +1,14 @@ +// +// KeyCenter.swift +// OpenLive +// +// Created by GongYuhua on 6/25/16. +// Copyright © 2016 Agora. All rights reserved. +// + +struct KeyCenter { + static let AppId: String = <#Your App Id#> + + // assign token to nil if you have not enabled app certificate + static var Token: String? = <#Temp Access Token#> +} diff --git a/macOS/APIExample/Commons/LogUtils.swift b/macOS/APIExample/Commons/LogUtils.swift new file mode 100644 index 000000000..bed190699 --- /dev/null +++ b/macOS/APIExample/Commons/LogUtils.swift @@ -0,0 +1,40 @@ +// +// LogViewController.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import Foundation + +enum LogLevel { + case info, warning, error + + var description: String { + switch self { + case .info: return "Info" + case .warning: return "Warning" + case .error: return "Error" + } + } +} + +struct LogItem { + var message:String + var level:LogLevel + var dateTime:Date +} + +class LogUtils { + static var logs:[LogItem] = [] + + static func log(message: String, level: LogLevel) { + LogUtils.logs.append(LogItem(message: message, level: level, dateTime: Date())) + print("\(level.description): \(message)") + } + + static func removeAll() { + LogUtils.logs.removeAll() + } +} diff --git a/macOS/APIExample/Commons/MetalVideoView.xib b/macOS/APIExample/Commons/MetalVideoView.xib new file mode 100644 index 000000000..5d2894581 --- /dev/null +++ b/macOS/APIExample/Commons/MetalVideoView.xib @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Commons/RawDataApi/AgoraMediaDataPlugin.h b/macOS/APIExample/Commons/RawDataApi/AgoraMediaDataPlugin.h new file mode 100644 index 000000000..4509ca33b --- /dev/null +++ b/macOS/APIExample/Commons/RawDataApi/AgoraMediaDataPlugin.h @@ -0,0 +1,89 @@ +// +// AgoraMediaDataPlugin.h +// OpenVideoCall +// +// Created by CavanSu on 26/02/2018. +// Copyright © 2018 Agora. All rights reserved. +// + +#import "AgoraMediaRawData.h" + +#if (!(TARGET_OS_IPHONE) && (TARGET_OS_MAC)) +#import +typedef NSImage AGImage; +#else +#import +typedef UIImage AGImage; +#endif + +typedef NS_OPTIONS(NSInteger, ObserverVideoType) { + ObserverVideoTypeCaptureVideo = 1 << 0, + ObserverVideoTypeRenderVideo = 1 << 1, + ObserverVideoTypePreEncodeVideo = 1 << 2 +}; + +typedef NS_OPTIONS(NSInteger, ObserverAudioType) { + ObserverAudioTypeRecordAudio = 1 << 0, + ObserverAudioTypePlaybackAudio = 1 << 1, + ObserverAudioTypePlaybackAudioFrameBeforeMixing = 1 << 2, + ObserverAudioTypeMixedAudio = 1 << 3 +}; + +typedef NS_OPTIONS(NSInteger, ObserverPacketType) { + ObserverPacketTypeSendAudio = 1 << 0, + ObserverPacketTypeSendVideo = 1 << 1, + ObserverPacketTypeReceiveAudio = 1 << 2, + ObserverPacketTypeReceiveVideo = 1 << 3 +}; + +@class AgoraRtcEngineKit; +@class AgoraMediaDataPlugin; +@protocol AgoraVideoDataPluginDelegate +@optional +- (AgoraVideoRawData * _Nonnull)mediaDataPlugin:(AgoraMediaDataPlugin * _Nonnull)mediaDataPlugin didCapturedVideoRawData:(AgoraVideoRawData * _Nonnull)videoRawData; +- (AgoraVideoRawData * _Nonnull)mediaDataPlugin:(AgoraMediaDataPlugin * _Nonnull)mediaDataPlugin willRenderVideoRawData:(AgoraVideoRawData * _Nonnull)videoRawData ofUid:(uint)uid; +- (AgoraVideoRawData * _Nonnull)mediaDataPlugin:(AgoraMediaDataPlugin * _Nonnull)mediaDataPlugin willPreEncodeVideoRawData:(AgoraVideoRawData * _Nonnull)videoRawData; + +@end + +@protocol AgoraAudioDataPluginDelegate +@optional +- (AgoraAudioRawData * _Nonnull)mediaDataPlugin:(AgoraMediaDataPlugin * _Nonnull)mediaDataPlugin didRecordAudioRawData:(AgoraAudioRawData * _Nonnull)audioRawData; +- (AgoraAudioRawData * _Nonnull)mediaDataPlugin:(AgoraMediaDataPlugin * _Nonnull)mediaDataPlugin willPlaybackAudioRawData:(AgoraAudioRawData * _Nonnull)audioRawData; +- (AgoraAudioRawData * _Nonnull)mediaDataPlugin:(AgoraMediaDataPlugin * _Nonnull)mediaDataPlugin willPlaybackBeforeMixingAudioRawData:(AgoraAudioRawData * _Nonnull)audioRawData ofUid:(uint)uid; +- (AgoraAudioRawData * _Nonnull)mediaDataPlugin:(AgoraMediaDataPlugin * _Nonnull)mediaDataPlugin didMixedAudioRawData:(AgoraAudioRawData * _Nonnull)audioRawData; +@end + +@protocol AgoraPacketDataPluginDelegate +@optional +- (AgoraPacketRawData * _Nonnull)mediaDataPlugin:(AgoraMediaDataPlugin * _Nonnull)mediaDataPlugin willSendAudioPacket:(AgoraPacketRawData * _Nonnull)audioPacket; +- (AgoraPacketRawData * _Nonnull)mediaDataPlugin:(AgoraMediaDataPlugin * _Nonnull)mediaDataPlugin willSendVideoPacket:(AgoraPacketRawData * _Nonnull)videoPacket; + +- (AgoraPacketRawData * _Nonnull)mediaDataPlugin:(AgoraMediaDataPlugin * _Nonnull)mediaDataPlugin didReceivedAudioPacket:(AgoraPacketRawData * _Nonnull)audioPacket; +- (AgoraPacketRawData * _Nonnull)mediaDataPlugin:(AgoraMediaDataPlugin * _Nonnull)mediaDataPlugin didReceivedVideoPacket:(AgoraPacketRawData * _Nonnull)videoPacket; +@end + +@interface AgoraMediaDataPlugin : NSObject +@property (nonatomic, weak) id _Nullable videoDelegate; +@property (nonatomic, weak) id _Nullable audioDelegate; +@property (nonatomic, weak) id _Nullable packetDelegate; + ++ (instancetype _Nonnull)mediaDataPluginWithAgoraKit:(AgoraRtcEngineKit * _Nonnull)agoraKit; + +- (void)registerVideoRawDataObserver:(ObserverVideoType)observerType; +- (void)deregisterVideoRawDataObserver:(ObserverVideoType)observerType; + +- (void)registerAudioRawDataObserver:(ObserverAudioType)observerType; +- (void)deregisterAudioRawDataObserver:(ObserverAudioType)observerType; + +- (void)registerPacketRawDataObserver:(ObserverPacketType)observerType; +- (void)deregisterPacketRawDataObserver:(ObserverPacketType)observerType; + +- (void)setVideoRawDataFormatter:(AgoraVideoRawDataFormatter * _Nonnull)formatter; +- (AgoraVideoRawDataFormatter * _Nonnull)getCurrentVideoRawDataFormatter; + +// you can call following methods before set videoDelegate +- (void)localSnapshot:(void (^ _Nullable)(AGImage * _Nonnull image))completion; +- (void)remoteSnapshotWithUid:(NSUInteger)uid image:(void (^ _Nullable)(AGImage * _Nonnull image))completion; +@end + diff --git a/macOS/APIExample/Commons/RawDataApi/AgoraMediaDataPlugin.mm b/macOS/APIExample/Commons/RawDataApi/AgoraMediaDataPlugin.mm new file mode 100644 index 000000000..90ee4fb6c --- /dev/null +++ b/macOS/APIExample/Commons/RawDataApi/AgoraMediaDataPlugin.mm @@ -0,0 +1,548 @@ +// +// AgoraMediaRawData.m +// OpenVideoCall +// +// Created by CavanSu on 26/02/2018. +// Copyright © 2018 Agora. All rights reserved. +// + +#import "AgoraMediaDataPlugin.h" + +#import +#import +#import +#include + +typedef void (^imageBlock)(AGImage *image); + +@interface AgoraMediaDataPlugin () +@property (nonatomic, assign) NSUInteger screenShotUid; +@property (nonatomic, assign) ObserverVideoType observerVideoType; +@property (nonatomic, assign) ObserverAudioType observerAudioType; +@property (nonatomic, assign) ObserverPacketType observerPacketType; +@property (nonatomic, strong) AgoraVideoRawDataFormatter *videoFormatter; +@property (nonatomic, weak) AgoraRtcEngineKit *agoraKit; +@property (nonatomic, copy) imageBlock imageBlock; +- (void)yuvToUIImageWithVideoRawData:(AgoraVideoRawData *)data; +@end + + +class AgoraVideoFrameObserver : public agora::media::IVideoFrameObserver +{ +public: + AgoraMediaDataPlugin *mediaDataPlugin; + BOOL getOneDidCaptureVideoFrame = false; + BOOL getOneWillRenderVideoFrame = false; + unsigned int videoFrameUid = -1; + + AgoraVideoRawData* getVideoRawDataWithVideoFrame(VideoFrame& videoFrame) + { + AgoraVideoRawData *data = [[AgoraVideoRawData alloc] init]; + data.type = videoFrame.type; + data.width = videoFrame.width; + data.height = videoFrame.height; + data.yStride = videoFrame.yStride; + data.uStride = videoFrame.uStride; + data.vStride = videoFrame.vStride; + data.rotation = videoFrame.rotation; + data.renderTimeMs = videoFrame.renderTimeMs; + data.yBuffer = (char *)videoFrame.yBuffer; + data.uBuffer = (char *)videoFrame.uBuffer; + data.vBuffer = (char *)videoFrame.vBuffer; + return data; + } + + void modifiedVideoFrameWithNewVideoRawData(VideoFrame& videoFrame, AgoraVideoRawData *videoRawData) + { + videoFrame.width = videoRawData.width; + videoFrame.height = videoRawData.height; + videoFrame.yStride = videoRawData.yStride; + videoFrame.uStride = videoRawData.uStride; + videoFrame.vStride = videoRawData.vStride; + videoFrame.rotation = videoRawData.rotation; + videoFrame.renderTimeMs = videoRawData.renderTimeMs; + } + + virtual bool onCaptureVideoFrame(VideoFrame& videoFrame) override + { + if (!mediaDataPlugin && ((mediaDataPlugin.observerVideoType >> 0) == 0)) return true; + @autoreleasepool { + AgoraVideoRawData *newData = nil; + if ([mediaDataPlugin.videoDelegate respondsToSelector:@selector(mediaDataPlugin:didCapturedVideoRawData:)]) { + AgoraVideoRawData *data = getVideoRawDataWithVideoFrame(videoFrame); + newData = [mediaDataPlugin.videoDelegate mediaDataPlugin:mediaDataPlugin didCapturedVideoRawData:data]; + modifiedVideoFrameWithNewVideoRawData(videoFrame, newData); + + // ScreenShot + if (getOneDidCaptureVideoFrame) { + getOneDidCaptureVideoFrame = false; + [mediaDataPlugin yuvToUIImageWithVideoRawData:newData]; + } + } + } + return true; + } + + virtual bool onPreEncodeVideoFrame(VideoFrame& videoFrame) override + { + if (!mediaDataPlugin && ((mediaDataPlugin.observerVideoType >> 2) == 0)) return true; + @autoreleasepool { + AgoraVideoRawData *newData = nil; + if ([mediaDataPlugin.videoDelegate respondsToSelector:@selector(mediaDataPlugin:willPreEncodeVideoRawData:)]) { + AgoraVideoRawData *data = getVideoRawDataWithVideoFrame(videoFrame); + newData = [mediaDataPlugin.videoDelegate mediaDataPlugin:mediaDataPlugin willPreEncodeVideoRawData:data]; + modifiedVideoFrameWithNewVideoRawData(videoFrame, newData); + } + } + return true; + } + + virtual bool onRenderVideoFrame(unsigned int uid, VideoFrame& videoFrame) override + { + if (!mediaDataPlugin && ((mediaDataPlugin.observerVideoType >> 1) == 0)) return true; + @autoreleasepool { + AgoraVideoRawData *newData = nil; + if ([mediaDataPlugin.videoDelegate respondsToSelector:@selector(mediaDataPlugin:willRenderVideoRawData:ofUid:)]) { + AgoraVideoRawData *data = getVideoRawDataWithVideoFrame(videoFrame); + newData = [mediaDataPlugin.videoDelegate mediaDataPlugin:mediaDataPlugin willRenderVideoRawData:data ofUid:uid]; + modifiedVideoFrameWithNewVideoRawData(videoFrame, newData); + + // ScreenShot + if (getOneWillRenderVideoFrame && videoFrameUid == uid) { + getOneWillRenderVideoFrame = false; + videoFrameUid = -1; + [mediaDataPlugin yuvToUIImageWithVideoRawData:newData]; + } + } + } + return true; + } + + virtual VIDEO_FRAME_TYPE getVideoFormatPreference() override + { + return VIDEO_FRAME_TYPE(mediaDataPlugin.videoFormatter.type); + } + + virtual bool getRotationApplied() override + { + return mediaDataPlugin.videoFormatter.rotationApplied; + } + + virtual bool getMirrorApplied() override + { + return mediaDataPlugin.videoFormatter.mirrorApplied; + } +}; + +class AgoraAudioFrameObserver : public agora::media::IAudioFrameObserver +{ +public: + AgoraMediaDataPlugin *mediaDataPlugin; + + AgoraAudioRawData* getAudioRawDataWithAudioFrame(AudioFrame& audioFrame) + { + AgoraAudioRawData *data = [[AgoraAudioRawData alloc] init]; + data.samples = audioFrame.samples; + data.bytesPerSample = audioFrame.bytesPerSample; + data.channels = audioFrame.channels; + data.samplesPerSec = audioFrame.samplesPerSec; + data.renderTimeMs = audioFrame.renderTimeMs; + data.buffer = (char *)audioFrame.buffer; + data.bufferSize = audioFrame.samples * audioFrame.bytesPerSample; + return data; + } + + void modifiedAudioFrameWithNewAudioRawData(AudioFrame& audioFrame, AgoraAudioRawData *audioRawData) + { + audioFrame.samples = audioRawData.samples; + audioFrame.bytesPerSample = audioRawData.bytesPerSample; + audioFrame.channels = audioRawData.channels; + audioFrame.samplesPerSec = audioRawData.samplesPerSec; + audioFrame.renderTimeMs = audioRawData.renderTimeMs; + } + + virtual bool onRecordAudioFrame(AudioFrame& audioFrame) override + { + if (!mediaDataPlugin && ((mediaDataPlugin.observerAudioType >> 0) == 0)) return true; + @autoreleasepool { + if ([mediaDataPlugin.audioDelegate respondsToSelector:@selector(mediaDataPlugin:didRecordAudioRawData:)]) { + AgoraAudioRawData *data = getAudioRawDataWithAudioFrame(audioFrame); + AgoraAudioRawData *newData = [mediaDataPlugin.audioDelegate mediaDataPlugin:mediaDataPlugin didRecordAudioRawData:data]; + modifiedAudioFrameWithNewAudioRawData(audioFrame, newData); + } + } + return true; + } + + virtual bool onPlaybackAudioFrame(AudioFrame& audioFrame) override + { + if (!mediaDataPlugin && ((mediaDataPlugin.observerAudioType >> 1) == 0)) return true; + @autoreleasepool { + if ([mediaDataPlugin.audioDelegate respondsToSelector:@selector(mediaDataPlugin:willPlaybackAudioRawData:)]) { + AgoraAudioRawData *data = getAudioRawDataWithAudioFrame(audioFrame); + AgoraAudioRawData *newData = [mediaDataPlugin.audioDelegate mediaDataPlugin:mediaDataPlugin willPlaybackAudioRawData:data]; + modifiedAudioFrameWithNewAudioRawData(audioFrame, newData); + } + } + return true; + } + + virtual bool onPlaybackAudioFrameBeforeMixing(unsigned int uid, AudioFrame& audioFrame) override + { + if (!mediaDataPlugin && ((mediaDataPlugin.observerAudioType >> 2) == 0)) return true; + @autoreleasepool { + if ([mediaDataPlugin.audioDelegate respondsToSelector:@selector(mediaDataPlugin:willPlaybackBeforeMixingAudioRawData:ofUid:)]) { + AgoraAudioRawData *data = getAudioRawDataWithAudioFrame(audioFrame); + AgoraAudioRawData *newData = [mediaDataPlugin.audioDelegate mediaDataPlugin:mediaDataPlugin willPlaybackBeforeMixingAudioRawData:data ofUid:uid]; + modifiedAudioFrameWithNewAudioRawData(audioFrame, newData); + } + } + return true; + } + + virtual bool onMixedAudioFrame(AudioFrame& audioFrame) override + { + if (!mediaDataPlugin && ((mediaDataPlugin.observerAudioType >> 3) == 0)) return true; + @autoreleasepool { + if ([mediaDataPlugin.audioDelegate respondsToSelector:@selector(mediaDataPlugin:didMixedAudioRawData:)]) { + AgoraAudioRawData *data = getAudioRawDataWithAudioFrame(audioFrame); + AgoraAudioRawData *newData = [mediaDataPlugin.audioDelegate mediaDataPlugin:mediaDataPlugin didMixedAudioRawData:data]; + modifiedAudioFrameWithNewAudioRawData(audioFrame, newData); + } + } + return true; + } +}; + +class AgoraPacketObserver : public agora::rtc::IPacketObserver +{ +public: + AgoraMediaDataPlugin *mediaDataPlugin; + + AgoraPacketObserver() + { + } + + AgoraPacketRawData* getPacketRawDataWithPacket(Packet& packet) + { + AgoraPacketRawData *data = [[AgoraPacketRawData alloc] init]; + data.buffer = packet.buffer; + data.bufferSize = packet.size; + return data; + } + + void modifiedPacketWithNewPacketRawData(Packet& packet, AgoraPacketRawData *rawData) + { + packet.size = rawData.bufferSize; + } + + virtual bool onSendAudioPacket(Packet& packet) + { + if (!mediaDataPlugin && ((mediaDataPlugin.observerPacketType >> 0) == 0)) return true; + @autoreleasepool { + if ([mediaDataPlugin.packetDelegate respondsToSelector:@selector(mediaDataPlugin:willSendAudioPacket:)]) { + AgoraPacketRawData *data = getPacketRawDataWithPacket(packet); + AgoraPacketRawData *newData = [mediaDataPlugin.packetDelegate mediaDataPlugin:mediaDataPlugin willSendAudioPacket:data]; + modifiedPacketWithNewPacketRawData(packet, newData); + } + } + return true; + } + + virtual bool onSendVideoPacket(Packet& packet) + { + if (!mediaDataPlugin && ((mediaDataPlugin.observerPacketType >> 1) == 0)) return true; + @autoreleasepool { + if ([mediaDataPlugin.packetDelegate respondsToSelector:@selector(mediaDataPlugin:willSendVideoPacket:)]) { + AgoraPacketRawData *data = getPacketRawDataWithPacket(packet); + AgoraPacketRawData *newData = [mediaDataPlugin.packetDelegate mediaDataPlugin:mediaDataPlugin willSendVideoPacket:data]; + modifiedPacketWithNewPacketRawData(packet, newData); + } + } + return true; + } + + virtual bool onReceiveAudioPacket(Packet& packet) + { + if (!mediaDataPlugin && ((mediaDataPlugin.observerPacketType >> 2) == 0)) return true; + @autoreleasepool { + if ([mediaDataPlugin.packetDelegate respondsToSelector:@selector(mediaDataPlugin:didReceivedAudioPacket:)]) { + AgoraPacketRawData *data = getPacketRawDataWithPacket(packet); + AgoraPacketRawData *newData = [mediaDataPlugin.packetDelegate mediaDataPlugin:mediaDataPlugin didReceivedAudioPacket:data]; + modifiedPacketWithNewPacketRawData(packet, newData); + } + } + return true; + } + + virtual bool onReceiveVideoPacket(Packet& packet) + { + if (!mediaDataPlugin && ((mediaDataPlugin.observerPacketType >> 3) == 0)) return true; + @autoreleasepool { + if ([mediaDataPlugin.packetDelegate respondsToSelector:@selector(mediaDataPlugin:didReceivedVideoPacket:)]) { + AgoraPacketRawData *data = getPacketRawDataWithPacket(packet); + AgoraPacketRawData *newData = [mediaDataPlugin.packetDelegate mediaDataPlugin:mediaDataPlugin didReceivedVideoPacket:data]; + modifiedPacketWithNewPacketRawData(packet, newData); + } + } + return true; + } +}; + +static AgoraVideoFrameObserver s_videoFrameObserver; +static AgoraAudioFrameObserver s_audioFrameObserver; +static AgoraPacketObserver s_packetObserver; + +@implementation AgoraMediaDataPlugin + ++ (instancetype)mediaDataPluginWithAgoraKit:(AgoraRtcEngineKit *)agoraKit { + AgoraMediaDataPlugin *source = [[AgoraMediaDataPlugin alloc] init]; + source.videoFormatter = [[AgoraVideoRawDataFormatter alloc] init]; + source.agoraKit = agoraKit; + + if (!agoraKit) { + return nil; + } + return source; +} + +- (void)registerVideoRawDataObserver:(ObserverVideoType)observerType { + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)self.agoraKit.getNativeHandle; + agora::util::AutoPtr mediaEngine; + mediaEngine.queryInterface(rtc_engine, agora::AGORA_IID_MEDIA_ENGINE); + + NSInteger oldValue = self.observerVideoType; + self.observerVideoType |= observerType; + + if (mediaEngine && oldValue == 0) + { + mediaEngine->registerVideoFrameObserver(&s_videoFrameObserver); + s_videoFrameObserver.mediaDataPlugin = self; + } +} + +- (void)deregisterVideoRawDataObserver:(ObserverVideoType)observerType { + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)self.agoraKit.getNativeHandle; + agora::util::AutoPtr mediaEngine; + mediaEngine.queryInterface(rtc_engine, agora::AGORA_IID_MEDIA_ENGINE); + + self.observerVideoType ^= observerType; + + if (mediaEngine && self.observerVideoType == 0) + { + mediaEngine->registerVideoFrameObserver(NULL); + s_videoFrameObserver.mediaDataPlugin = nil; + } +} + +- (void)registerAudioRawDataObserver:(ObserverAudioType)observerType { + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)self.agoraKit.getNativeHandle; + agora::util::AutoPtr mediaEngine; + mediaEngine.queryInterface(rtc_engine, agora::AGORA_IID_MEDIA_ENGINE); + + NSInteger oldValue = self.observerAudioType; + self.observerAudioType |= observerType; + + if (mediaEngine && oldValue == 0) + { + mediaEngine->registerAudioFrameObserver(&s_audioFrameObserver); + s_audioFrameObserver.mediaDataPlugin = self; + } +} + +- (void)deregisterAudioRawDataObserver:(ObserverAudioType)observerType { + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)self.agoraKit.getNativeHandle; + agora::util::AutoPtr mediaEngine; + mediaEngine.queryInterface(rtc_engine, agora::AGORA_IID_MEDIA_ENGINE); + + self.observerAudioType ^= observerType; + + if (mediaEngine && self.observerAudioType == 0) + { + mediaEngine->registerAudioFrameObserver(NULL); + s_audioFrameObserver.mediaDataPlugin = nil; + } +} + +- (void)registerPacketRawDataObserver:(ObserverPacketType)observerType { + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)self.agoraKit.getNativeHandle; + + NSInteger oldValue = self.observerPacketType; + self.observerPacketType |= observerType; + + if (rtc_engine && oldValue == 0) + { + rtc_engine->registerPacketObserver(&s_packetObserver); + s_packetObserver.mediaDataPlugin = self; + } +} + +- (void)deregisterPacketRawDataObserver:(ObserverPacketType)observerType { + agora::rtc::IRtcEngine* rtc_engine = (agora::rtc::IRtcEngine*)self.agoraKit.getNativeHandle; + + self.observerPacketType ^= observerType; + + if (rtc_engine && self.observerPacketType == 0) + { + rtc_engine->registerPacketObserver(NULL); + s_packetObserver.mediaDataPlugin = nil; + } +} + +- (void)setVideoRawDataFormatter:(AgoraVideoRawDataFormatter * _Nonnull)formatter { + if (self.videoFormatter.type != formatter.type) { + self.videoFormatter.type = formatter.type; + } + + if (self.videoFormatter.rotationApplied != formatter.rotationApplied) { + self.videoFormatter.rotationApplied = formatter.rotationApplied; + } + + if (self.videoFormatter.mirrorApplied != formatter.mirrorApplied) { + self.videoFormatter.mirrorApplied = formatter.mirrorApplied; + } +} + +- (AgoraVideoRawDataFormatter * _Nonnull)getCurrentVideoRawDataFormatter { + return self.videoFormatter; +} + +#pragma mark - Screen Capture +- (void)localSnapshot:(void (^ _Nullable)(AGImage * _Nonnull image))completion { + self.imageBlock = completion; + s_videoFrameObserver.getOneDidCaptureVideoFrame = true; +} + +- (void)remoteSnapshotWithUid:(NSUInteger)uid image:(void (^ _Nullable)(AGImage * _Nonnull image))completion { + self.imageBlock = completion; + s_videoFrameObserver.getOneWillRenderVideoFrame = true; + s_videoFrameObserver.videoFrameUid = (unsigned int)uid; +} + +- (void)yuvToUIImageWithVideoRawData:(AgoraVideoRawData *)data { + + int height = data.height; + int yStride = data.yStride; + + char* yBuffer = data.yBuffer; + char* uBuffer = data.uBuffer; + char* vBuffer = data.vBuffer; + + int Len = yStride * data.height * 3/2; + int yLength = yStride * data.height; + int uLength = yLength / 4; + + unsigned char * buf = (unsigned char *)malloc(Len); + memcpy(buf, yBuffer, yLength); + memcpy(buf + yLength, uBuffer, uLength); + memcpy(buf + yLength + uLength, vBuffer, uLength); + + unsigned char * NV12buf = (unsigned char *)malloc(Len); + [self yuv420p_to_nv12:buf nv12:NV12buf width:yStride height:height]; + @autoreleasepool { + [self UIImageToJpg:NV12buf width:yStride height:height rotation:data.rotation]; + } + if(buf != NULL) { + free(buf); + buf = NULL; + } + + if(NV12buf != NULL) { + free(NV12buf); + NV12buf = NULL; + } + +} + +// Agora SDK Raw Data format is YUV420P +- (void)yuv420p_to_nv12:(unsigned char*)yuv420p nv12:(unsigned char*)nv12 width:(int)width height:(int)height { + int i, j; + int y_size = width * height; + + unsigned char* y = yuv420p; + unsigned char* u = yuv420p + y_size; + unsigned char* v = yuv420p + y_size * 5 / 4; + + unsigned char* y_tmp = nv12; + unsigned char* uv_tmp = nv12 + y_size; + + // y + memcpy(y_tmp, y, y_size); + + // u + for (j = 0, i = 0; j < y_size * 0.5; j += 2, i++) { + // swtich the location of U、V,to NV12 or NV21 +#if 1 + uv_tmp[j] = u[i]; + uv_tmp[j+1] = v[i]; +#else + uv_tmp[j] = v[i]; + uv_tmp[j+1] = u[i]; +#endif + } +} + +- (void)UIImageToJpg:(unsigned char *)buffer width:(int)width height:(int)height rotation:(int)rotation { + AGImage *image = [self YUVtoUIImage:width h:height buffer:buffer rotation: rotation]; + if (self.imageBlock) { + self.imageBlock(image); + } +} + +//This is API work well for NV12 data format only. +- (AGImage *)YUVtoUIImage:(int)w h:(int)h buffer:(unsigned char *)buffer rotation:(int)rotation { + //YUV(NV12)-->CIImage--->UIImage Conversion + NSDictionary *pixelAttributes = @{(NSString*)kCVPixelBufferIOSurfacePropertiesKey:@{}}; + CVPixelBufferRef pixelBuffer = NULL; + CVReturn result = CVPixelBufferCreate(kCFAllocatorDefault, + w, + h, + kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + (__bridge CFDictionaryRef)(pixelAttributes), + &pixelBuffer); + CVPixelBufferLockBaseAddress(pixelBuffer,0); + void *yDestPlane = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0); + + // Here y_ch0 is Y-Plane of YUV(NV12) data. + unsigned char *y_ch0 = buffer; + unsigned char *y_ch1 = buffer + w * h; + memcpy(yDestPlane, y_ch0, w * h); + void *uvDestPlane = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1); + + // Here y_ch1 is UV-Plane of YUV(NV12) data. + memcpy(uvDestPlane, y_ch1, w * h * 0.5); + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0); + + if (result != kCVReturnSuccess) { + NSLog(@"Unable to create cvpixelbuffer %d", result); + } + + // CIImage Conversion + CIImage *coreImage = [CIImage imageWithCVPixelBuffer:pixelBuffer]; + CIContext *temporaryContext = [CIContext contextWithOptions:nil]; + CGImageRef videoImage = [temporaryContext createCGImage:coreImage + fromRect:CGRectMake(0, 0, w, h)]; + +#if (!(TARGET_OS_IPHONE) && (TARGET_OS_MAC)) + AGImage *finalImage = [[NSImage alloc] initWithCGImage:videoImage size:NSMakeSize(w, h)]; +#else + + UIImageOrientation imageOrientation; + switch (rotation) { + case 0: imageOrientation = UIImageOrientationUp; break; + case 90: imageOrientation = UIImageOrientationRight; break; + case 180: imageOrientation = UIImageOrientationDown; break; + case 270: imageOrientation = UIImageOrientationLeft; break; + default: imageOrientation = UIImageOrientationUp; break; + } + + AGImage *finalImage = [[AGImage alloc] initWithCGImage:videoImage + scale:1.0 + orientation:imageOrientation]; +#endif + CVPixelBufferRelease(pixelBuffer); + CGImageRelease(videoImage); + return finalImage; +} +@end + diff --git a/macOS/APIExample/Commons/RawDataApi/AgoraMediaRawData.h b/macOS/APIExample/Commons/RawDataApi/AgoraMediaRawData.h new file mode 100644 index 000000000..a60375557 --- /dev/null +++ b/macOS/APIExample/Commons/RawDataApi/AgoraMediaRawData.h @@ -0,0 +1,44 @@ +// +// AgoraVideoRawData.h +// OpenVideoCall +// +// Created by CavanSu on 26/02/2018. +// Copyright © 2018 Agora. All rights reserved. +// + +#import + +@interface AgoraVideoRawDataFormatter : NSObject +@property (nonatomic, assign) int type; //YUV 420, YUV 422P, RGBA +@property (nonatomic, assign) BOOL rotationApplied; +@property (nonatomic, assign) BOOL mirrorApplied; +@end + +@interface AgoraVideoRawData : NSObject +@property (nonatomic, assign) int type; +@property (nonatomic, assign) int width; //width of video frame +@property (nonatomic, assign) int height; //height of video frame +@property (nonatomic, assign) int yStride; //stride of Y data buffer +@property (nonatomic, assign) int uStride; //stride of U data buffer +@property (nonatomic, assign) int vStride; //stride of V data buffer +@property (nonatomic, assign) int rotation; // rotation of this frame (0, 90, 180, 270) +@property (nonatomic, assign) int64_t renderTimeMs; // timestamp +@property (nonatomic, assign) char* yBuffer; //Y data buffer +@property (nonatomic, assign) char* uBuffer; //U data buffer +@property (nonatomic, assign) char* vBuffer; //V data buffer +@end + +@interface AgoraAudioRawData : NSObject +@property (nonatomic, assign) int samples; //number of samples in this frame +@property (nonatomic, assign) int bytesPerSample; //number of bytes per sample: 2 for PCM16 +@property (nonatomic, assign) int channels; //number of channels (data are interleaved if stereo) +@property (nonatomic, assign) int samplesPerSec; //sampling rate +@property (nonatomic, assign) int bufferSize; +@property (nonatomic, assign) int64_t renderTimeMs; +@property (nonatomic, assign) char* buffer; //data buffer +@end + +@interface AgoraPacketRawData : NSObject +@property (nonatomic, assign) const unsigned char* buffer; +@property (nonatomic, assign) uint bufferSize; +@end diff --git a/macOS/APIExample/Commons/RawDataApi/AgoraMediaRawData.m b/macOS/APIExample/Commons/RawDataApi/AgoraMediaRawData.m new file mode 100644 index 000000000..7d43ddbfb --- /dev/null +++ b/macOS/APIExample/Commons/RawDataApi/AgoraMediaRawData.m @@ -0,0 +1,32 @@ +// +// AgoraVideoRawData.m +// OpenVideoCall +// +// Created by CavanSu on 26/02/2018. +// Copyright © 2018 Agora. All rights reserved. +// + +#import "AgoraMediaRawData.h" + +@implementation AgoraVideoRawDataFormatter +- (instancetype)init { + if (self = [super init]) { + self.mirrorApplied = false; + self.rotationApplied = false; + self.type = 0; + } + return self; +} +@end + +@implementation AgoraVideoRawData + +@end + +@implementation AgoraAudioRawData + +@end + +@implementation AgoraPacketRawData + +@end diff --git a/macOS/APIExample/Commons/Settings/SettingCells.swift b/macOS/APIExample/Commons/Settings/SettingCells.swift new file mode 100644 index 000000000..8042034c9 --- /dev/null +++ b/macOS/APIExample/Commons/Settings/SettingCells.swift @@ -0,0 +1,57 @@ +// +// SettingCells.swift +// APIExample +// +// Created by XC on 2020/12/15. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import Foundation + +class SettingsBaseParam: NSObject { + var key: String + var label: String + var type: String + + init(type: String, key: String, label: String) { + self.type = type + self.key = key + self.label = label + } +} + +class SettingBaseCell: NSTableCellView { + var configs: T? + weak var delegate: SettingsViewControllerDelegate? + + func configure(config: T) { + self.configs = config + } +} + +class SettingsSelectParam: SettingsBaseParam { + var value: String + var settingItem: SettingItem + weak var context: NSViewController? + + init(key: String, label: String, settingItem: SettingItem, context: NSViewController) { + self.settingItem = settingItem + self.context = context + self.value = settingItem.selectedOption().label + super.init(type: "SelectCell", key: key, label: label) + } +} + +class SettingSelectCell: SettingBaseCell> { + @IBOutlet weak var label: NSTextField? + @IBOutlet weak var picker: NSPopUpButton! + + override func configure(config: SettingsSelectParam) { + super.configure(config: config) + self.label?.cell?.title = config.label + self.picker?.addItems(withTitles: config.settingItem.options.map({ (option: SettingItemOption) -> String in + return option.label + })) + self.picker?.selectItem(at: config.settingItem.selected) + } +} diff --git a/macOS/APIExample/Commons/Settings/SettingsViewController.swift b/macOS/APIExample/Commons/Settings/SettingsViewController.swift new file mode 100644 index 000000000..b93b78541 --- /dev/null +++ b/macOS/APIExample/Commons/Settings/SettingsViewController.swift @@ -0,0 +1,13 @@ +// +// SettingsViewController.swift +// APIExample +// +// Created by XC on 2020/12/15. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import Foundation + +protocol SettingsViewControllerDelegate: AnyObject { + func didChangeValue(type: String, key: String, value: Any) +} diff --git a/macOS/APIExample/Commons/StatisticsInfo.swift b/macOS/APIExample/Commons/StatisticsInfo.swift new file mode 100755 index 000000000..2c6a2b82d --- /dev/null +++ b/macOS/APIExample/Commons/StatisticsInfo.swift @@ -0,0 +1,197 @@ +// +// MediaInfo.swift +// OpenVideoCall +// +// Created by GongYuhua on 4/11/16. +// Copyright © 2016 Agora. All rights reserved. +// + +import Foundation +import AgoraRtcKit + +struct StatisticsInfo { + struct LocalInfo { + var channelStats : AgoraChannelStats? + var videoStats : AgoraRtcLocalVideoStats? + var audioStats : AgoraRtcLocalAudioStats? + var audioVolume : UInt? + } + + struct RemoteInfo { + var videoStats : AgoraRtcRemoteVideoStats? + var audioStats : AgoraRtcRemoteAudioStats? + var audioVolume : UInt? + } + + enum StatisticsType { + case local(LocalInfo), remote(RemoteInfo) + + var isLocal: Bool { + switch self { + case .local: return true + case .remote: return false + } + } + } + + var type: StatisticsType + + init(type: StatisticsType) { + self.type = type + } + + mutating func updateChannelStats(_ stats: AgoraChannelStats) { + guard self.type.isLocal else { + return + } + switch type { + case .local(let info): + var new = info + new.channelStats = stats + self.type = .local(new) + default: + break + } + } + + mutating func updateLocalVideoStats(_ stats: AgoraRtcLocalVideoStats) { + guard self.type.isLocal else { + return + } + switch type { + case .local(let info): + var new = info + new.videoStats = stats + self.type = .local(new) + default: + break + } + } + + mutating func updateLocalAudioStats(_ stats: AgoraRtcLocalAudioStats) { + guard self.type.isLocal else { + return + } + switch type { + case .local(let info): + var new = info + new.audioStats = stats + self.type = .local(new) + default: + break + } + } + + mutating func updateVideoStats(_ stats: AgoraRtcRemoteVideoStats) { + switch type { + case .remote(let info): + var new = info + new.videoStats = stats +// dimension = CGSize(width: Int(stats.width), height: Int(stats.height)) +// fps = stats.rendererOutputFrameRate + self.type = .remote(new) + default: + break + } + } + + mutating func updateAudioStats(_ stats: AgoraRtcRemoteAudioStats) { + switch type { + case .remote(let info): + var new = info + new.audioStats = stats + self.type = .remote(new) + default: + break + } + } + + mutating func updateVolume(_ volume: UInt) { + switch type { + case .local(let info): + var new = info + new.audioVolume = volume + self.type = .local(new) + case .remote(let info): + var new = info + new.audioVolume = volume + self.type = .remote(new) + } + } + + func description(audioOnly:Bool) -> String { + var full: String + switch type { + case .local(let info): full = localDescription(info: info, audioOnly: audioOnly) + case .remote(let info): full = remoteDescription(info: info, audioOnly: audioOnly) + } + return full + } + + func localDescription(info: LocalInfo, audioOnly: Bool) -> String { + var results:[String] = [] + + if(!audioOnly) { + if let volume = info.audioVolume { + results.append("Volume: \(volume)") + } + + if let videoStats = info.videoStats, let channelStats = info.channelStats, let audioStats = info.audioStats { + results.append("\(Int(videoStats.encodedFrameWidth))×\(Int(videoStats.encodedFrameHeight)),\(videoStats.sentFrameRate)fps") + results.append("LM Delay: \(channelStats.lastmileDelay)ms") + results.append("VSend: \(videoStats.sentBitrate)kbps") + results.append("ASend: \(audioStats.sentBitrate)kbps") + results.append("CPU: \(channelStats.cpuAppUsage)%/\(channelStats.cpuTotalUsage)%") + results.append("VSend Loss: \(videoStats.txPacketLossRate)%") + results.append("ASend Loss: \(audioStats.txPacketLossRate)%") + } + } else { + if let volume = info.audioVolume { + results.append("Volume: \(volume)") + } + + if let channelStats = info.channelStats, let audioStats = info.audioStats { + results.append("LM Delay: \(channelStats.lastmileDelay)ms") + results.append("ASend: \(audioStats.sentBitrate)kbps") + results.append("CPU: \(channelStats.cpuAppUsage)%/\(channelStats.cpuTotalUsage)%") + results.append("ASend Loss: \(audioStats.txPacketLossRate)%") + } + } + + return results.joined(separator: "\n") + } + + func remoteDescription(info: RemoteInfo, audioOnly: Bool) -> String { + var results:[String] = [] + + + if(!audioOnly) { + if let volume = info.audioVolume { + results.append("Volume: \(volume)") + } + + if let videoStats = info.videoStats, let audioStats = info.audioStats { + let audioQuality:AgoraNetworkQuality = AgoraNetworkQuality(rawValue: audioStats.quality) ?? .unknown + results.append("\(Int(videoStats.width))×\(Int(videoStats.height)),\(videoStats.decoderOutputFrameRate)fps") + results.append("VRecv: \(videoStats.receivedBitrate)kbps") + results.append("ARecv: \(audioStats.receivedBitrate)kbps") + results.append("VLoss: \(videoStats.packetLossRate)%") + results.append("ALoss: \(audioStats.audioLossRate)%") + results.append("AQuality: \(audioQuality.description())") + } + } else { + if let volume = info.audioVolume { + results.append("Volume: \(volume)") + } + + if let audioStats = info.audioStats { + let audioQuality:AgoraNetworkQuality = AgoraNetworkQuality(rawValue: audioStats.quality) ?? .unknown + results.append("ARecv: \(audioStats.receivedBitrate)kbps") + results.append("ALoss: \(audioStats.audioLossRate)%") + results.append("AQuality: \(audioQuality.description())") + } + } + + return results.joined(separator: "\n") + } +} diff --git a/macOS/APIExample/Commons/VideoView.swift b/macOS/APIExample/Commons/VideoView.swift new file mode 100644 index 000000000..d3f640add --- /dev/null +++ b/macOS/APIExample/Commons/VideoView.swift @@ -0,0 +1,84 @@ +// +// VideoView.swift +// OpenVideoCall +// +// Created by GongYuhua on 2/14/16. +// Copyright © 2016 Agora. All rights reserved. +// + +import Cocoa + +protocol NibLoadable { + static var nibName: String? { get } + static func createFromNib(in bundle: Bundle) -> Self? +} + +extension NibLoadable where Self: NSView { + + static var nibName: String? { + return String(describing: Self.self) + } + + static func createFromNib(in bundle: Bundle = Bundle.main) -> Self? { + guard let nibName = nibName else { return nil } + var topLevelArray: NSArray? = nil + bundle.loadNibNamed(NSNib.Name(nibName), owner: self, topLevelObjects: &topLevelArray) + guard let results = topLevelArray else { return nil } + let views = Array(results).filter { $0 is Self } + return views.last as? Self + } +} + +class VideoView: NSView, NibLoadable { + @IBOutlet weak var placeholder: NSTextField! + @IBOutlet weak var videocanvas: NSView! + @IBOutlet weak var infolabel: NSTextField! + @IBOutlet weak var statsLabel:NSTextField! + + var uid:UInt? { + didSet { + infolabel.stringValue = uid == nil ? "" : "\(uid!)" + } + } + + + var audioOnly:Bool = false + enum StreamType { + case local + case remote + + func isLocal() -> Bool{ + switch self { + case .local: return true + case .remote: return false + } + } + } + var statsInfo:StatisticsInfo? { + didSet{ + guard let stats = statsInfo else {return} + statsLabel.stringValue = stats.description(audioOnly: audioOnly) + } + } + var type:StreamType? + + override func awakeFromNib() { + super.awakeFromNib() + } +} + +class MetalVideoView: NSView,NibLoadable { + @IBOutlet weak var placeholder: NSTextField! + @IBOutlet weak var videocanvas: AgoraMetalRender! + @IBOutlet weak var infolabel: NSTextField! + + var uid:UInt? { + didSet { + infolabel.stringValue = uid == nil ? "" : "\(uid!)" + } + } + + override func awakeFromNib() { + super.awakeFromNib() + } +} diff --git a/macOS/APIExample/Commons/VideoView.xib b/macOS/APIExample/Commons/VideoView.xib new file mode 100644 index 000000000..a9fa0ec94 --- /dev/null +++ b/macOS/APIExample/Commons/VideoView.xib @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Commons/WindowsCenter.swift b/macOS/APIExample/Commons/WindowsCenter.swift new file mode 100644 index 000000000..704b958a4 --- /dev/null +++ b/macOS/APIExample/Commons/WindowsCenter.swift @@ -0,0 +1,251 @@ +// +// WindowsCenter.swift +// AgoraVideoCall +// +// Created by GongYuhua on 6/14/16. +// Copyright © 2016 Agora. All rights reserved. +// + +import CoreGraphics + +#if os(iOS) +import UIKit +#else +import Cocoa +#endif + +enum WindowType: Int { + case window, screen +} + +enum ApplicationType { + case web, ppt, keynote, word, pages, preview, other +} + +class Window { + fileprivate(set) var type: WindowType = .window + fileprivate(set) var id: UInt32 = 0 + fileprivate(set) var name: String! + fileprivate(set) var image: NSImage! + fileprivate(set) var width: CGFloat = 0 + fileprivate(set) var height: CGFloat = 0 + var size: CGSize { + return CGSize(width: width, height: height) + } + + init?(windowDic: NSDictionary) { + if let layerNumber = windowDic[Window.convertCFString(kCGWindowLayer)] { + let cfNumber = layerNumber as! CFNumber + let layer = Window.convertCFNumber(cfNumber) + guard layer == 0 else { + return nil + } + } + + if let alphaNumber = windowDic[Window.convertCFString(kCGWindowAlpha)] { + let cfNumber = alphaNumber as! CFNumber + let alpha = Window.convertCFNumber(cfNumber) + if alpha == 0 { + return nil + } + } + + if windowDic[Window.convertCFString(kCGWindowName)] == nil { + return nil + } + + guard let idNumber = windowDic[Window.convertCFString(kCGWindowNumber)] else { + return nil + } + + let cfNumber = idNumber as! CFNumber + let id = Window.convertCFNumber(cfNumber) + + var name: String? + if let ownerName = windowDic[Window.convertCFString(kCGWindowOwnerName)] { + let cfName: CFString = ownerName as! CFString + name = Window.convertCFString(cfName) + if name == "Agora Video Call" { + return nil + } + } + + guard let image = Window.image(of: id) else { + return nil + } + + self.id = id + self.name = name ?? "Unknown" + self.image = image + self.width = image.size.width + self.height = image.size.height + self.type = .window + } + + init?(screenId: CGDirectDisplayID, name: String) { + self.name = name + self.id = screenId + self.type = .screen + guard let image = Window.imageOfScreenId(self.id) else { + return + } + self.image = image + self.width = image.size.width + self.height = image.size.height + } + + fileprivate init() {} + + static func fullScreenWindow() -> Window { + let window = Window() + window.name = "Full Screen" + window.image = imageOfFullScreen() + if let main = NSScreen.screens.first { + let scale = main.backingScaleFactor + window.width = main.frame.size.width * scale + window.height = main.frame.size.height * scale + } + + return window + } + + static func image(of windowId: CGWindowID) -> NSImage? { + if let screenShot = CGWindowListCreateImage(CGRect.null, .optionIncludingWindow, CGWindowID(windowId), CGWindowImageOption.boundsIgnoreFraming) { + let bitmapRep = NSBitmapImageRep(cgImage: screenShot) + let image = NSImage() + image.addRepresentation(bitmapRep) + + if image.size.width == 1 { + return nil + } else { + return image + } + } else { + return nil + } + } + + fileprivate static func imageOfScreenId(_ screenId: CGDirectDisplayID) -> NSImage? { + if let screenShot = CGDisplayCreateImage(screenId) { + let bitmapRep = NSBitmapImageRep(cgImage: screenShot) + let image = NSImage() + image.addRepresentation(bitmapRep) + + if image.size.width == 1 { + return nil + } else { + return image + } + } else { + return nil + } + } + + fileprivate static func imageOfFullScreen() -> NSImage { + if let screenShot = CGWindowListCreateImage(CGRect.infinite, .optionOnScreenOnly, CGWindowID(0), CGWindowImageOption()) { + let bitmapRep = NSBitmapImageRep(cgImage: screenShot) + let image = NSImage() + image.addRepresentation(bitmapRep) + return image + } else { + return NSImage() + } + } +} + +class WindowList { + var items = [Window]() + + func getList() { + var list = [Window]() + + var webList = [Window]() + var pptList = [Window]() + var keynoteList = [Window]() + var wordList = [Window]() + var pagesList = [Window]() + var previewList = [Window]() + var otherList = [Window]() + + // add screens + let screens = NSScreen.screens + for (index, screen) in screens.enumerated() { + guard let screenId = screen.deviceDescription[NSDeviceDescriptionKey(rawValue: "NSScreenNumber")] as? CGDirectDisplayID else { + continue + } + if let window = Window(screenId: screenId, name: "Screen \(index + 1)") { + list.append(window) + } + } + + // add windows + if let windowDicCFArray = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], 0) { + let windowDicList = windowDicCFArray as NSArray + + for windowElement in windowDicList { + let windowDic = windowElement + if let windowDic = windowDic as? NSDictionary { + if let window = Window(windowDic: windowDic) { + let appType = typeOfApplication(with: window.name) + switch appType { + case .web: + webList.append(window) + case .ppt: + pptList.append(window) + case .keynote: + keynoteList.append(window) + case .word: + wordList.append(window) + case .pages: + pagesList.append(window) + case .preview: + previewList.append(window) + case .other: + otherList.append(window) + } + } + } + } + } + let temp = webList + pptList + keynoteList + wordList + list += temp + pagesList + previewList + otherList + + self.items = list + } + + private func typeOfApplication(with name: String) -> ApplicationType { + if name.contains("Google Chrome") || name.contains("Safari") { + return .web + } else if name.contains("PowerPoint") { + return .ppt + } else if name.contains("Microsoft") { + return .word + } else if name.contains("Keynote") { + return .keynote + } else if name.contains("Pages") { + return .pages + } else if name.contains("Preview") { + return .preview + } else { + return .other + } + } + + private func isHighPriortyWindow(with name: String) -> Bool { + return (name.contains("Microsoft") && !name.contains("Outlook") && !name.contains("Teams")) + || name.contains("Google Chrome") + } +} + +extension Window { + class func convertCFString(_ cfString: CFString) -> String { + let string = cfString as NSString + return string as String + + } + + class func convertCFNumber(_ cfNumber: CFNumber) -> UInt32 { + let number = cfNumber as NSNumber + return number.uint32Value + } +} diff --git a/macOS/APIExample/Examples/Advanced/AudioMixing/AudioMixing.swift b/macOS/APIExample/Examples/Advanced/AudioMixing/AudioMixing.swift new file mode 100644 index 000000000..a9d01e472 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/AudioMixing/AudioMixing.swift @@ -0,0 +1,605 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import Cocoa +import AgoraRtcKit +import AGEVideoLayout + +class AudioMixing: BaseViewController { + let EFFECT_ID:Int32 = 1 + let EFFECT_ID_2:Int32 = 2 + var videos: [VideoView] = [] + + @IBOutlet weak var container: AGEVideoContainer! + @IBOutlet weak var startAudioMixingBtn: NSButton! + @IBOutlet weak var pauseAudioMixingBtn: NSButton! + @IBOutlet weak var resumeAudioMixingBtn: NSButton! + @IBOutlet weak var stopAudioMixingBtn: NSButton! + @IBOutlet weak var audioMixingProgress: NSProgressIndicator! + @IBOutlet weak var audioMixingDuration: NSTextField! + + var agoraKit: AgoraRtcEngineKit! + var timer:Timer? + + /** + --- Audio Profile Picker --- + */ + @IBOutlet weak var selectAudioProfilePicker: Picker! + var audioProfiles = AgoraAudioProfile.allValues() + var selectedProfile: AgoraAudioProfile? { + let index = selectAudioProfilePicker.indexOfSelectedItem + if index >= 0 && index < audioProfiles.count { + return audioProfiles[index] + } else { + return nil + } + } + func initSelectAudioProfilePicker() { + selectAudioProfilePicker.label.stringValue = "Audio Profile".localized + selectAudioProfilePicker.picker.addItems(withTitles: audioProfiles.map { $0.description() }) + + selectAudioProfilePicker.onSelectChanged { + if !self.isJoined { + return + } + guard let profile = self.selectedProfile, + let scenario = self.selectedAudioScenario else { + return + } + self.agoraKit.setAudioProfile(profile, scenario: scenario) + } + } + + /** + --- Audio Scenario Picker --- + */ + @IBOutlet weak var selectAudioScenarioPicker: Picker! + var audioScenarios = AgoraAudioScenario.allValues() + var selectedAudioScenario: AgoraAudioScenario? { + let index = self.selectAudioScenarioPicker.indexOfSelectedItem + if index >= 0 && index < Configs.Resolutions.count { + return audioScenarios[index] + } else { + return nil + } + } + func initSelectAudioScenarioPicker() { + selectAudioScenarioPicker.label.stringValue = "Audio Scenario".localized + selectAudioScenarioPicker.picker.addItems(withTitles: audioScenarios.map { $0.description() }) + + selectAudioScenarioPicker.onSelectChanged { + if !self.isJoined { + return + } + guard let profile = self.selectedProfile, + let scenario = self.selectedAudioScenario else { + return + } + self.agoraKit.setAudioProfile(profile, scenario: scenario) + } + } + + /** + --- Microphones Picker --- + */ + @IBOutlet weak var selectMicsPicker: Picker! + var mics:[AgoraRtcDeviceInfo] = [] { + didSet { + DispatchQueue.main.async {[unowned self] in + self.selectMicsPicker.picker.addItems(withTitles: self.mics.map {$0.deviceName ?? "unknown"}) + } + } + } + var selectedMicrophone: AgoraRtcDeviceInfo? { + let index = self.selectMicsPicker.indexOfSelectedItem + if index >= 0 && index < mics.count { + return mics[index] + } else { + return nil + } + } + func initSelectMicsPicker() { + selectMicsPicker.label.stringValue = "Microphone".localized + // find device in a separate thread to avoid blocking main thread + let queue = DispatchQueue(label: "device.enumerateDevices") + queue.async {[unowned self] in + self.mics = self.agoraKit.enumerateDevices(.audioRecording) ?? [] + } + + selectMicsPicker.onSelectChanged { + if !self.isJoined { + return + } + // use selected devices + guard let micId = self.selectedMicrophone?.deviceId else { + return + } + self.agoraKit.setDevice(.audioRecording, deviceId: micId) + } + } + + /** + --- Layout Picker --- + */ + @IBOutlet weak var selectLayoutPicker: Picker! + let layouts = [Layout("1v1", 2), Layout("1v3", 4), Layout("1v8", 9), Layout("1v15", 16)] + var selectedLayout: Layout? { + let index = self.selectLayoutPicker.indexOfSelectedItem + if index >= 0 && index < layouts.count { + return layouts[index] + } else { + return nil + } + } + func initSelectLayoutPicker() { + layoutVideos(2) + selectLayoutPicker.label.stringValue = "Layout".localized + selectLayoutPicker.picker.addItems(withTitles: layouts.map { $0.label }) + selectLayoutPicker.onSelectChanged { + if self.isJoined { + return + } + guard let layout = self.selectedLayout else { return } + self.layoutVideos(layout.value) + } + } + + /** + --- mix volume slider + */ + @IBOutlet weak var mixingVolumeSlider: Slider! + func initMixingVolumeSlider() { + mixingVolumeSlider.label.stringValue = "Mixing Volume".localized + mixingVolumeSlider.slider.minValue = 0 + mixingVolumeSlider.slider.maxValue = 100 + mixingVolumeSlider.slider.intValue = 50 + + mixingVolumeSlider.onSliderChanged { + let value: Int = Int(self.mixingVolumeSlider.slider.intValue) + LogUtils.log(message: "onAudioMixingVolumeChanged \(value)", level: .info) + self.agoraKit.adjustAudioMixingVolume(value) + } + } + + /** + --- Mixing Playback Volume --- + */ + @IBOutlet weak var mixingPlaybackVolumeSlider: Slider! + func initMixingPlaybackVolumeSlider() { + mixingPlaybackVolumeSlider.label.stringValue = "Mixing Playback Volume".localized + mixingPlaybackVolumeSlider.slider.minValue = 0 + mixingPlaybackVolumeSlider.slider.maxValue = 100 + mixingPlaybackVolumeSlider.slider.intValue = 50 + + mixingPlaybackVolumeSlider.onSliderChanged { + let value: Int = Int(self.mixingPlaybackVolumeSlider.slider.intValue) + LogUtils.log(message: "onAudioMixingPlaybackVolumeChanged \(value)", level: .info) + self.agoraKit.adjustAudioMixingPlayoutVolume(value) + } + } + + /** + --- Mixing Publish Volume --- + */ + @IBOutlet weak var mixingPublishVolumeSlider: Slider! + func initMixingPublishVolumeSlider() { + mixingPublishVolumeSlider.label.stringValue = "Mixing Publish Volume".localized + mixingPublishVolumeSlider.slider.minValue = 0 + mixingPublishVolumeSlider.slider.maxValue = 100 + mixingPublishVolumeSlider.slider.intValue = 50 + + mixingPublishVolumeSlider.onSliderChanged { + let value: Int = Int(self.mixingPublishVolumeSlider.slider.intValue) + LogUtils.log(message: "onAudioMixingPublishVolumeChanged \(value)", level: .info) + self.agoraKit.adjustAudioMixingPublishVolume(value) + } + } + + /** + --- effectVolumeSlider --- + */ + @IBOutlet weak var effectVolumeSlider: Slider! + func initEffectVolumeSlider() { + effectVolumeSlider.label.stringValue = "Overall Effect Volume".localized + effectVolumeSlider.slider.minValue = 0 + effectVolumeSlider.slider.maxValue = 100 + effectVolumeSlider.slider.intValue = 50 + + effectVolumeSlider.onSliderChanged { + let value: Double = Double(self.effectVolumeSlider.slider.intValue) + LogUtils.log(message: "onAudioEffectVolumeChanged \(value)", level: .info) + self.agoraKit.setEffectsVolume(value) + } + } + @IBOutlet weak var playAudioEffectBtn: NSButton! + @IBAction func onPlayEffect(_ sender:NSButton){ + if let filepath = Bundle.main.path(forResource: "audioeffect", ofType: "mp3") { + let result = agoraKit.playEffect(EFFECT_ID, filePath: filepath, loopCount: -1, pitch: 1, pan: 0, gain: 100, publish: true) + if result != 0 { + self.showAlert(title: "Error", message: "playEffect call failed: \(result), please check your params") + } + } + } + @IBOutlet weak var pauseAudioEffectBtn: NSButton! + @IBAction func onPauseEffect(_ sender:NSButton){ + let result = agoraKit.pauseEffect(EFFECT_ID) + if result != 0 { + self.showAlert(title: "Error", message: "pauseEffect call failed: \(result), please check your params") + } + } + @IBOutlet weak var resumeAudioEffectBtn: NSButton! + @IBAction func onResumeEffect(_ sender:NSButton){ + let result = agoraKit.resumeEffect(EFFECT_ID) + if result != 0 { + self.showAlert(title: "Error", message: "resumeEffect call failed: \(result), please check your params") + } + } + @IBOutlet weak var stopAudioEffectBtn: NSButton! + @IBAction func onStopEffect(_ sender:NSButton){ + let result = agoraKit.stopEffect(EFFECT_ID) + if result != 0 { + self.showAlert(title: "Error", message: "stopEffect call failed: \(result), please check your params") + } + } + + /** + --- Additional Effect Volume Slider --- + */ + @IBOutlet weak var additionalEffectVolumeSlider: Slider! + func initAdditionalEffectVolumeSlider() { + additionalEffectVolumeSlider.label.stringValue = "Additional Effect Volume".localized + additionalEffectVolumeSlider.slider.minValue = 0 + additionalEffectVolumeSlider.slider.maxValue = 100 + additionalEffectVolumeSlider.slider.intValue = 50 + + additionalEffectVolumeSlider.onSliderChanged { + let value: Double = Double(self.additionalEffectVolumeSlider.slider.intValue) + LogUtils.log(message: "onAudioEffectVolumeChanged \(value)", level: .info) + self.agoraKit.setVolumeOfEffect(self.EFFECT_ID_2, withVolume: value) + } + } + /** + --- Play Additional Effect Button --- + */ + @IBOutlet weak var playAdditionalEffectButton: NSButton! + @IBOutlet weak var stopAdditionalEffectButton: NSButton! + @IBAction func onPlayEffect2(_ sender:NSButton){ + if let filepath = Bundle.main.path(forResource: "effectA", ofType: "wav") { + let result = agoraKit.playEffect(EFFECT_ID_2, filePath: filepath, loopCount: -1, pitch: 1, pan: 0, gain: 100, publish: true) + if result != 0 { + self.showAlert(title: "Error", message: "playEffect call failed: \(result), please check your params") + } + } + } + @IBAction func onStopEffect2(_ sender:NSButton){ + let result = agoraKit.stopEffect(EFFECT_ID_2) + if result != 0 { + self.showAlert(title: "Error", message: "stopEffect call failed: \(result), please check your params") + } + } + /** + --- Channel TextField --- + */ + @IBOutlet weak var channelField: Input! + func initChannelField() { + channelField.label.stringValue = "Channel".localized + channelField.field.placeholderString = "Channel Name".localized + } + + /** + --- Button --- + */ + @IBOutlet weak var joinChannelButton: NSButton! + func initJoinChannelButton() { + joinChannelButton.title = isJoined ? "Leave Channel".localized : "Join Channel".localized + } + + // indicate if current instance has joined channel + var isJoined: Bool = false { + didSet { + channelField.isEnabled = !isJoined + selectLayoutPicker.isEnabled = !isJoined + initJoinChannelButton() + } + } + + // indicate for doing something + var isProcessing: Bool = false { + didSet { + joinChannelButton.isEnabled = !isProcessing + } + } + + override func viewDidLoad() { + super.viewDidLoad() + // Do view setup here. + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + initSelectAudioProfilePicker() + initSelectAudioScenarioPicker() + initSelectMicsPicker() + initSelectLayoutPicker() + + initMixingVolumeSlider() + initMixingPlaybackVolumeSlider() + initMixingPublishVolumeSlider() + initAdditionalEffectVolumeSlider() + initEffectVolumeSlider() + + initChannelField() + initJoinChannelButton() + } + + override func viewWillBeRemovedFromSplitView() { + if isJoined { + agoraKit.leaveChannel { (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + } + } + } + + @IBAction func onJoinPressed(_ sender:Any) { + if !isJoined { + // check configuration + let channel = channelField.stringValue + if channel.isEmpty { + return + } + // use selected devices + guard let micId = selectedMicrophone?.deviceId, + let profile = selectedProfile, + let scenario = selectedAudioScenario else { + return + } + agoraKit.setDevice(.audioRecording, deviceId: micId) + // disable video module in audio scene + agoraKit.disableVideo() + agoraKit.setAudioProfile(profile, scenario: scenario) + // set live broadcaster mode + agoraKit.setChannelProfile(.liveBroadcasting) + // set myself as broadcaster to stream audio + agoraKit.setClientRole(.broadcaster) + + // enable volume indicator + agoraKit.enableAudioVolumeIndication(200, smooth: 3, report_vad: false) + + // update slider values + mixingPlaybackVolumeSlider.slider.doubleValue = Double(agoraKit.getAudioMixingPlayoutVolume()) + mixingPublishVolumeSlider.slider.doubleValue = Double(agoraKit.getAudioMixingPublishVolume()) + effectVolumeSlider.slider.doubleValue = Double(agoraKit.getEffectsVolume()) + additionalEffectVolumeSlider.slider.doubleValue = Double(agoraKit.getEffectsVolume()) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + isProcessing = true + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channel, info: nil, uid: 0, options: option) + if result != 0 { + isProcessing = false + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } else { + isProcessing = true + agoraKit.leaveChannel { [unowned self] (stats:AgoraChannelStats) in + self.isProcessing = false + LogUtils.log(message: "Left channel", level: .info) + self.videos[0].uid = nil + self.isJoined = false + self.videos.forEach { + $0.uid = nil + $0.statsLabel.stringValue = "" + } + } + } + } + + func startProgressTimer() { + // begin timer to update progress + if timer == nil { + timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { [weak self](timer:Timer) in + guard let weakself = self else {return} + let progress = Double(weakself.agoraKit.getAudioMixingCurrentPosition()) / Double(weakself.agoraKit.getAudioMixingDuration()) + weakself.audioMixingProgress.doubleValue = progress + let left = weakself.agoraKit.getAudioMixingDuration() - weakself.agoraKit.getAudioMixingCurrentPosition() + 1 + let seconds = left / 1000 + weakself.audioMixingDuration.stringValue = "\(String(format: "%02d", seconds / 60)) : \(String(format: "%02d", seconds % 60))" + }) + } + } + + func stopProgressTimer() { + // stop timer + if timer != nil { + timer?.invalidate() + timer = nil + } + } + + func updateTotalDuration(reset: Bool) { + if reset { + audioMixingProgress.doubleValue = 0 + audioMixingDuration.stringValue = "00 : 00" + } else { + let duration = agoraKit.getAudioMixingDuration() + let seconds = duration / 1000 + audioMixingDuration.stringValue = "\(String(format: "%02d", seconds / 60)) : \(String(format: "%02d", seconds % 60))" + } + } + + @IBAction func onStartAudioMixing(_ sender: NSButton) { + if let filepath = Bundle.main.path(forResource: "audiomixing", ofType: "mp3") { + let result = agoraKit.startAudioMixing(filepath, loopback: false, replace: false, cycle: -1) + if result != 0 { + self.showAlert(title: "Error", message: "startAudioMixing call failed: \(result), please check your params") + } else { + startProgressTimer() + updateTotalDuration(reset: false) + } + } + } + + @IBAction func onStopAudioMixing(_ sender:NSButton){ + let result = agoraKit.stopAudioMixing() + if result != 0 { + self.showAlert(title: "Error", message: "stopAudioMixing call failed: \(result), please check your params") + } else { + stopProgressTimer() + updateTotalDuration(reset: true) + } + } + + @IBAction func onPauseAudioMixing(_ sender:NSButton){ + let result = agoraKit.pauseAudioMixing() + if result != 0 { + self.showAlert(title: "Error", message: "pauseAudioMixing call failed: \(result), please check your params") + } else { + stopProgressTimer() + } + } + + @IBAction func onResumeAudioMixing(_ sender:NSButton){ + let result = agoraKit.resumeAudioMixing() + if result != 0 { + self.showAlert(title: "Error", message: "resumeAudioMixing call failed: \(result), please check your params") + } else { + startProgressTimer() + } + } + + func layoutVideos(_ count: Int) { + videos = [] + for i in 0...count - 1 { + let view = VideoView.createFromNib()! + if(i == 0) { + view.placeholder.stringValue = "Local" + view.type = .local + view.statsInfo = StatisticsInfo(type: .local(StatisticsInfo.LocalInfo())) + } else { + view.placeholder.stringValue = "Remote \(i)" + view.type = .remote + view.statsInfo = StatisticsInfo(type: .remote(StatisticsInfo.RemoteInfo())) + } + view.audioOnly = true + videos.append(view) + } + // layout render view + container.layoutStream(views: videos) + } +} + +/// agora rtc engine delegate events +extension AudioMixing: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + if isProcessing { + isProcessing = false + } + self.showAlert(title: "Error", message: "Error \(errorCode.rawValue) occur") + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isProcessing = false + isJoined = true + let localVideo = videos[0] + localVideo.uid = uid + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // find a VideoView w/o uid assigned + if let remoteVideo = videos.first(where: { $0.uid == nil }) { + remoteVideo.uid = uid + } else { + LogUtils.log(message: "no video canvas available for \(uid), cancel bind", level: .warning) + } + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + if let remoteVideo = videos.first(where: { $0.uid == uid }) { + remoteVideo.uid = nil + } else { + LogUtils.log(message: "no matching video canvas for \(uid), cancel unbind", level: .warning) + } + } + + + /// Reports the statistics of the current call. The SDK triggers this callback once every two seconds after the user joins the channel. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, reportRtcStats stats: AgoraChannelStats) { + videos[0].statsInfo?.updateChannelStats(stats) + } + + /// Reports the statistics of the uploading local audio streams once every two seconds. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, localAudioStats stats: AgoraRtcLocalAudioStats) { + videos[0].statsInfo?.updateLocalAudioStats(stats) + } + + /// Reports the statistics of the audio stream from each remote user/host. + /// @param stats stats struct for current call statistics + func rtcEngine(_ engine: AgoraRtcEngineKit, remoteAudioStats stats: AgoraRtcRemoteAudioStats) { + videos.first(where: { $0.uid == stats.uid })?.statsInfo?.updateAudioStats(stats) + } + + /// Reports which users are speaking, the speakers' volumes, and whether the local user is speaking. + /// @params speakers volume info for all speakers + /// @params totalVolume Total volume after audio mixing. The value range is [0,255]. + func rtcEngine(_ engine: AgoraRtcEngineKit, reportAudioVolumeIndicationOfSpeakers speakers: [AgoraRtcAudioVolumeInfo], totalVolume: Int) { + for volumeInfo in speakers { + if (volumeInfo.uid == 0) { + videos[0].statsInfo?.updateVolume(volumeInfo.volume) + } else { + videos.first(where: { $0.uid == volumeInfo.uid })?.statsInfo?.updateVolume(volumeInfo.volume) + } + } + } +} diff --git a/macOS/APIExample/Examples/Advanced/AudioMixing/Base.lproj/AudioMixing.storyboard b/macOS/APIExample/Examples/Advanced/AudioMixing/Base.lproj/AudioMixing.storyboard new file mode 100644 index 000000000..9590f4c3c --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/AudioMixing/Base.lproj/AudioMixing.storyboard @@ -0,0 +1,350 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings b/macOS/APIExample/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings new file mode 100644 index 000000000..6b6caa2cb --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/AudioMixing/zh-Hans.lproj/AudioMixing.strings @@ -0,0 +1,69 @@ + +/* Class = "NSButtonCell"; title = "Resume"; ObjectID = "8GX-mr-P4n"; */ +"8GX-mr-P4n.title" = "恢复播放"; + +/* Class = "NSTextFieldCell"; title = "00 : 00"; ObjectID = "8Kf-Su-NKI"; */ +"8Kf-Su-NKI.title" = "00 : 00"; + +/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "8bV-OK-zbc"; */ +"8bV-OK-zbc.title" = "1V15"; + +/* Class = "NSTextFieldCell"; title = "Audio Effect Controls"; ObjectID = "EBL-gG-Ubf"; */ +"EBL-gG-Ubf.title" = "音效控制"; + +/* Class = "NSTextFieldCell"; placeholderString = "加入频道"; ObjectID = "EhX-UJ-wov"; */ +"EhX-UJ-wov.placeholderString" = "输入频道名"; + +/* Class = "NSButtonCell"; title = "Play"; ObjectID = "IUe-EM-mfG"; */ +"IUe-EM-mfG.title" = "播放"; + +/* Class = "NSMenuItem"; title = "1V3"; ObjectID = "J6a-ul-c2H"; */ +"J6a-ul-c2H.title" = "1V3"; + +/* Class = "NSButtonCell"; title = "Pause"; ObjectID = "LgF-bS-HZ9"; */ +"LgF-bS-HZ9.title" = "暂停"; + +/* Class = "NSButtonCell"; title = "Join"; ObjectID = "P4E-oB-5Di"; */ +"P4E-oB-5Di.title" = "加入频道"; + +/* Class = "NSButtonCell"; title = "Stop"; ObjectID = "PAO-8S-8lX"; */ +"PAO-8S-8lX.title" = "停止"; + +/* Class = "NSButtonCell"; title = "Resume"; ObjectID = "R5O-SE-8mk"; */ +"R5O-SE-8mk.title" = "恢复播放"; + +/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "ch0-OR-L16"; */ +"ch0-OR-L16.title" = "1V1"; + +/* Class = "NSButtonCell"; title = "Stop"; ObjectID = "eUh-bN-yCK"; */ +"eUh-bN-yCK.title" = "停止"; + +/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "gWk-wf-hPu"; */ +"gWk-wf-hPu.title" = "1V8"; + +/* Class = "NSTextFieldCell"; title = "Mixing Volume"; ObjectID = "hQ4-2Z-Twn"; */ +"hQ4-2Z-Twn.title" = "混音音量"; + +/* Class = "NSViewController"; title = "Join Channel Audio"; ObjectID = "jAv-ZA-ecf"; */ +"jAv-ZA-ecf.title" = "Join Channel Audio"; + +/* Class = "NSTextFieldCell"; title = "Effect Volume"; ObjectID = "kh5-ZD-Sm3"; */ +"kh5-ZD-Sm3.title" = "音效音量"; + +/* Class = "NSTextFieldCell"; title = "Mixing Playback Volume"; ObjectID = "m1U-uA-7L4"; */ +"m1U-uA-7L4.title" = "混音播放音量"; + +/* Class = "NSButtonCell"; title = "Pause"; ObjectID = "mcr-Pl-O4W"; */ +"mcr-Pl-O4W.title" = "暂停"; + +/* Class = "NSTextFieldCell"; title = "Mixing Publish Volume"; ObjectID = "pHa-mK-6Ko"; */ +"pHa-mK-6Ko.title" = "混音发布音量"; + +/* Class = "NSButtonCell"; title = "Start"; ObjectID = "pNA-hI-TUH"; */ +"pNA-hI-TUH.title" = "开始混音"; + +/* Class = "NSTextFieldCell"; title = "Audio Mixing Controls"; ObjectID = "sLt-IU-VEu"; */ +"sLt-IU-VEu.title" = "混音控制"; + +/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "szu-uz-G6W"; */ +"szu-uz-G6W.title" = "离开频道"; diff --git a/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/Base.lproj/ChannelMediaRelay.storyboard b/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/Base.lproj/ChannelMediaRelay.storyboard new file mode 100644 index 000000000..b21e3cd75 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/Base.lproj/ChannelMediaRelay.storyboard @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/ChannelMediaRelay.swift b/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/ChannelMediaRelay.swift new file mode 100644 index 000000000..fae763d0e --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/ChannelMediaRelay.swift @@ -0,0 +1,306 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import Cocoa +import AgoraRtcKit +import AGEVideoLayout + +class ChannelMediaRelay: BaseViewController { + var videos: [VideoView] = [] + + @IBOutlet weak var Container: AGEVideoContainer! + + var agoraKit: AgoraRtcEngineKit! + + /** + --- Channel TextField --- + */ + @IBOutlet weak var channelField: Input! + func initChannelField() { + channelField.label.stringValue = "Channel".localized + channelField.field.placeholderString = "Channel Name".localized + } + + /** + --- Join Button --- + */ + @IBOutlet weak var joinChannelButton: NSButton! + func initJoinChannelButton() { + joinChannelButton.title = isJoined ? "Leave Channel".localized : "Join Channel".localized + } + + /** + --- Replay Channel TextField --- + */ + @IBOutlet weak var relayChannelField: Input! + func initRelayChannelField() { + relayChannelField.label.stringValue = "Relay Channel".localized + relayChannelField.field.placeholderString = "Relay Channnel Name".localized + } + + /** + --- Join Button --- + */ + @IBOutlet weak var relayButton: NSButton! + func initRelayButton() { + relayButton.title = isRelaying ? "Stop Relay".localized : "Start Relay".localized + } + @IBAction func onRelayPressed(_ sender: Any) { + if isProcessing { return } + if !isRelaying { + let destinationChannelName = relayChannelField.stringValue + // prevent operation if target channel name is empty + if(destinationChannelName.isEmpty) { + self.showAlert(message: "Destination channel name is empty") + return + } + // configure source info, channel name defaults to current, and uid defaults to local + let config = AgoraChannelMediaRelayConfiguration() + config.sourceInfo = AgoraChannelMediaRelayInfo(token: nil) + isProcessing = true + // configure target channel info + let destinationInfo = AgoraChannelMediaRelayInfo(token: nil) + config.setDestinationInfo(destinationInfo, forChannelName: destinationChannelName) + agoraKit.startChannelMediaRelay(config) + } else { + isProcessing = true + agoraKit.stopChannelMediaRelay() + } + + } + + // indicate if current instance has joined channel + var isJoined: Bool = false { + didSet { + channelField.isEnabled = !isJoined + initJoinChannelButton() + } + } + + // indicate for doing something + var isProcessing: Bool = false { + didSet { + joinChannelButton.isEnabled = !isProcessing + relayButton.isEnabled = !isProcessing + } + } + + var isRelaying: Bool = false { + didSet { + initRelayButton() + } + } + + override func viewDidLoad() { + super.viewDidLoad() + layoutVideos(2) + // Do view setup here. + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + agoraKit.enableVideo() + + initRelayChannelField() + initRelayButton() + initChannelField() + initJoinChannelButton() + } + + override func viewWillBeRemovedFromSplitView() { + if isJoined { + agoraKit.leaveChannel { (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + } + } + } + + @IBAction func onJoinPressed(_ sender:Any) { + if !isJoined { + // check configuration + let channel = channelField.stringValue + if channel.isEmpty { + return + } + // set live broadcaster mode + agoraKit.setChannelProfile(.liveBroadcasting) + // set myself as broadcaster to stream video/audio + agoraKit.setClientRole(.broadcaster) + + // set up local video to render your local camera preview + let localVideo = videos[0] + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + // the view to be binded + videoCanvas.view = localVideo.videocanvas + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + + let resolution = Configs.Resolutions[GlobalSettings.shared.resolutionSetting.selectedOption().value] + let fps = Configs.Fps[GlobalSettings.shared.fpsSetting.selectedOption().value] + agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + isProcessing = true + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channel, info: nil, uid: 0, options: option) + if result != 0 { + isProcessing = false + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } else { + isProcessing = true + agoraKit.leaveChannel { [unowned self] (stats:AgoraChannelStats) in + self.isProcessing = false + LogUtils.log(message: "Left channel", level: .info) + self.videos[0].uid = nil + self.isJoined = false + self.videos.forEach { + $0.uid = nil + $0.statsLabel.stringValue = "" + } + } + } + } + + func layoutVideos(_ count: Int) { + videos = [] + for i in 0...count - 1 { + let view = VideoView.createFromNib()! + if(i == 0) { + view.placeholder.stringValue = "Local" + } else { + view.placeholder.stringValue = "Remote \(i)" + } + videos.append(view) + } + // layout render view + Container.layoutStream(views: videos) + } +} + +/// agora rtc engine delegate events +extension ChannelMediaRelay: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + if isProcessing { + isProcessing = false + } + self.showAlert(title: "Error", message: "Error \(errorCode.rawValue) occur") + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isProcessing = false + isJoined = true + let localVideo = videos[0] + localVideo.uid = uid + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // find a VideoView w/o uid assigned + if let remoteVideo = videos.first(where: { $0.uid == nil }) { + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videocanvas + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + remoteVideo.uid = uid + } else { + LogUtils.log(message: "no video canvas available for \(uid), cancel bind", level: .warning) + } + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + if let remoteVideo = videos.first(where: { $0.uid == uid }) { + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + remoteVideo.uid = nil + } else { + LogUtils.log(message: "no matching video canvas for \(uid), cancel unbind", level: .warning) + } + } + + /// callback when a media relay process state changed + /// @param state state of media relay + /// @param error error details if media relay reaches failure state + func rtcEngine(_ engine: AgoraRtcEngineKit, channelMediaRelayStateDidChange state: AgoraChannelMediaRelayState, error: AgoraChannelMediaRelayError) { + LogUtils.log(message: "channelMediaRelayStateDidChange: \(state.rawValue) error \(error.rawValue)", level: .info) + isProcessing = false + switch state { + case .running: + isRelaying = true + case .failure: + showAlert(message: "Media Relay Failed: \(error.rawValue)") + isRelaying = false + case .idle: + isRelaying = false + default:break + } + } + + /// callback when a media relay event received + /// @param event event of media relay + func rtcEngine(_ engine: AgoraRtcEngineKit, didReceive event: AgoraChannelMediaRelayEvent) { + LogUtils.log(message: "didReceiveRelayEvent: \(event.rawValue)", level: .info) + } +} diff --git a/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/zh-Hans.lproj/ChannelMediaRelay.strings b/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/zh-Hans.lproj/ChannelMediaRelay.strings new file mode 100644 index 000000000..5e7d2da26 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/ChannelMediaRelay/zh-Hans.lproj/ChannelMediaRelay.strings @@ -0,0 +1,21 @@ + +/* Class = "NSTextFieldCell"; placeholderString = "Relay Channnel Name"; ObjectID = "Ab2-sI-Ld3"; */ +"Ab2-sI-Ld3.placeholderString" = "目标转发频道名"; + +/* Class = "NSButtonCell"; title = "Stop Relay"; ObjectID = "Hvn-10-7hC"; */ +"Hvn-10-7hC.title" = "停止转发"; + +/* Class = "NSViewController"; title = "Join Multiple Channels"; ObjectID = "IBJ-wZ-9Xx"; */ +"IBJ-wZ-9Xx.title" = "Join Multiple Channels"; + +/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "Xtr-fU-GZ5"; */ +"Xtr-fU-GZ5.title" = "离开频道"; + +/* Class = "NSButtonCell"; title = "Join"; ObjectID = "Zjl-Vt-wOj"; */ +"Zjl-Vt-wOj.title" = "加入频道"; + +/* Class = "NSTextFieldCell"; placeholderString = "加入频道"; ObjectID = "p0a-zy-yqS"; */ +"p0a-zy-yqS.placeholderString" = "输入频道名"; + +/* Class = "NSButtonCell"; title = "Start Relay"; ObjectID = "u6j-cJ-1Pe"; */ +"u6j-cJ-1Pe.title" = "开始转发"; diff --git a/macOS/APIExample/Examples/Advanced/CreateDataStream/Base.lproj/CreateDataStream.storyboard b/macOS/APIExample/Examples/Advanced/CreateDataStream/Base.lproj/CreateDataStream.storyboard new file mode 100644 index 000000000..2e77dcc80 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/CreateDataStream/Base.lproj/CreateDataStream.storyboard @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Examples/Advanced/CreateDataStream/CreateDataStream.swift b/macOS/APIExample/Examples/Advanced/CreateDataStream/CreateDataStream.swift new file mode 100644 index 000000000..ecd10221e --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/CreateDataStream/CreateDataStream.swift @@ -0,0 +1,323 @@ +// +// CreateDataStream.swift +// APIExample +// +// Created by XC on 2020/12/28. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import Cocoa +import AgoraRtcKit +import AGEVideoLayout + +class CreateDataStream: BaseViewController { + var videos: [VideoView] = [] + + @IBOutlet weak var Container: AGEVideoContainer! + + @IBOutlet weak var listMessagesView: NSTextField! + var messages: [String] = [] + func receiveMessage(message: String) { + if messages.count > 5 { + messages.remove(at: 0) + } + messages.append(message) + listMessagesView.stringValue = messages.joined(separator: "\n") + } + func removeAllMessages() { + messages.removeAll() + listMessagesView.stringValue = "" + } + + var agoraKit: AgoraRtcEngineKit! + + /** + --- Channel TextField --- + */ + @IBOutlet weak var channelField: Input! + func initChannelField() { + channelField.label.stringValue = "Channel".localized + channelField.field.placeholderString = "Channel Name".localized + } + + /** + --- Join Button --- + */ + @IBOutlet weak var joinChannelButton: NSButton! + func initJoinChannelButton() { + joinChannelButton.title = isJoined ? "Leave Channel".localized : "Join Channel".localized + } + + /** + --- Data TextField --- + */ + @IBOutlet weak var inputStringField: Input! + func initInputStringField() { + inputStringField.isEnabled = false + inputStringField.label.stringValue = "Send Message".localized + inputStringField.field.placeholderString = "Input Message".localized + } + + /** + --- Send Button --- + */ + @IBOutlet weak var sendButton: NSButton! + func initSendButton() { + sendButton.isEnabled = isJoined && !isSending + sendButton.title = isSending ? "Sending".localized : "Send".localized + } + @IBAction func onSendPressed(_ sender: Any) { + if !isSending { + let message = inputStringField.stringValue + if message.isEmpty { + return + } + isSending = true + if !streamCreated { + // create the data stream + // Each user can create up to five data streams during the lifecycle of the agoraKit + let config = AgoraDataStreamConfig() + let result = agoraKit.createDataStream(&streamId, config: config) + if result != 0 { + isSending = false + self.showAlert(title: "Error", message: "createDataStream call failed: \(result), please check your params") + } else { + streamCreated = true + } + } + + let result = agoraKit.sendStreamMessage(streamId, data: Data(message.utf8)) + if result != 0 { + self.showAlert(title: "Error", message: "sendStreamMessage call failed: \(result), please check your params") + } else { + inputStringField.stringValue = "" + } + isSending = false + } + } + + var streamCreated = false + var streamId: Int = 0 + + // indicate if current instance has joined channel + var isJoined: Bool = false { + didSet { + channelField.isEnabled = !isJoined + initJoinChannelButton() + inputStringField.isEnabled = isJoined && !isSending + sendButton.isEnabled = isJoined && !isSending + } + } + + // indicate for doing something + var isProcessing: Bool = false { + didSet { + joinChannelButton.isEnabled = !isProcessing + sendButton.isEnabled = !isProcessing + } + } + + var isSending: Bool = false { + didSet { + initSendButton() + } + } + + override func viewDidLoad() { + super.viewDidLoad() + layoutVideos(2) + // Do view setup here. + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + agoraKit.enableVideo() + + initInputStringField() + initSendButton() + initChannelField() + initJoinChannelButton() + } + + override func viewWillBeRemovedFromSplitView() { + if isJoined { + agoraKit.leaveChannel { (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + } + } + } + + @IBAction func onJoinPressed(_ sender:Any) { + if !isJoined { + // check configuration + let channel = channelField.stringValue + if channel.isEmpty { + return + } + // set live broadcaster mode + agoraKit.setChannelProfile(.liveBroadcasting) + // set myself as broadcaster to stream video/audio + agoraKit.setClientRole(.broadcaster) + + // set up local video to render your local camera preview + let localVideo = videos[0] + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + // the view to be binded + videoCanvas.view = localVideo.videocanvas + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + + let resolution = Configs.Resolutions[GlobalSettings.shared.resolutionSetting.selectedOption().value] + let fps = Configs.Fps[GlobalSettings.shared.fpsSetting.selectedOption().value] + agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + isProcessing = true + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channel, info: nil, uid: 0, options: option) + if result != 0 { + isProcessing = false + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } else { + isProcessing = true + removeAllMessages() + agoraKit.leaveChannel { [unowned self] (stats:AgoraChannelStats) in + self.isProcessing = false + LogUtils.log(message: "Left channel", level: .info) + self.videos[0].uid = nil + self.isJoined = false + self.videos.forEach { + $0.uid = nil + $0.statsLabel.stringValue = "" + } + self.streamCreated = false + } + } + } + + func layoutVideos(_ count: Int) { + videos = [] + for i in 0...count - 1 { + let view = VideoView.createFromNib()! + if(i == 0) { + view.placeholder.stringValue = "Local" + } else { + view.placeholder.stringValue = "Remote \(i)" + } + videos.append(view) + } + // layout render view + Container.layoutStream(views: videos) + } +} + +/// agora rtc engine delegate events +extension CreateDataStream: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + if isProcessing { + isProcessing = false + } + self.showAlert(title: "Error", message: "Error \(errorCode.rawValue) occur") + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isProcessing = false + isJoined = true + let localVideo = videos[0] + localVideo.uid = uid + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // find a VideoView w/o uid assigned + if let remoteVideo = videos.first(where: { $0.uid == nil }) { + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videocanvas + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + remoteVideo.uid = uid + } else { + LogUtils.log(message: "no video canvas available for \(uid), cancel bind", level: .warning) + } + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + if let remoteVideo = videos.first(where: { $0.uid == uid }) { + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + remoteVideo.uid = nil + } else { + LogUtils.log(message: "no matching video canvas for \(uid), cancel unbind", level: .warning) + } + } + + func rtcEngine(_ engine: AgoraRtcEngineKit, receiveStreamMessageFromUid uid: UInt, streamId: Int, data: Data) { + let message = String.init(data: data, encoding: .utf8) ?? "" + receiveMessage(message: "from: \(uid) message: \(message)") + LogUtils.log(message: "receiveStreamMessageFromUid: \(uid) \(message)", level: .info) + } + + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurStreamMessageErrorFromUid uid: UInt, streamId: Int, error: Int, missed: Int, cached: Int) { + LogUtils.log(message: "didOccurStreamMessageErrorFromUid: \(uid), error \(error), missed \(missed), cached \(cached)", level: .info) + showAlert(message: "didOccurStreamMessageErrorFromUid: \(uid)") + } +} diff --git a/macOS/APIExample/Examples/Advanced/CreateDataStream/zh-Hans.lproj/CreateDataStream.strings b/macOS/APIExample/Examples/Advanced/CreateDataStream/zh-Hans.lproj/CreateDataStream.strings new file mode 100644 index 000000000..b7362217e --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/CreateDataStream/zh-Hans.lproj/CreateDataStream.strings @@ -0,0 +1,12 @@ + +/* Class = "NSBox"; title = "Box"; ObjectID = "I4o-9l-2Vv"; */ +"I4o-9l-2Vv.title" = "Box"; + +/* Class = "NSButtonCell"; title = "Send"; ObjectID = "eYM-ow-8en"; */ +"eYM-ow-8en.title" = "发送"; + +/* Class = "NSTextFieldCell"; title = "Received Messages"; ObjectID = "mGf-09-ljc"; */ +"mGf-09-ljc.title" = "收到的消息"; + +/* Class = "NSButtonCell"; title = "Join"; ObjectID = "mmH-hT-gAv"; */ +"mmH-hT-gAv.title" = "加入频道"; diff --git a/macOS/APIExample/Examples/Advanced/CustomAudioRender/Base.lproj/CustomAudioRender.storyboard b/macOS/APIExample/Examples/Advanced/CustomAudioRender/Base.lproj/CustomAudioRender.storyboard new file mode 100644 index 000000000..939d63233 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/CustomAudioRender/Base.lproj/CustomAudioRender.storyboard @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Examples/Advanced/CustomAudioRender/CustomAudioRender.swift b/macOS/APIExample/Examples/Advanced/CustomAudioRender/CustomAudioRender.swift new file mode 100644 index 000000000..fb23a335a --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/CustomAudioRender/CustomAudioRender.swift @@ -0,0 +1,283 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import Cocoa +import AgoraRtcKit +import AGEVideoLayout + +class CustomAudioRender: BaseViewController { + + var agoraKit: AgoraRtcEngineKit! + var exAudio: ExternalAudio = ExternalAudio.shared() + + var videos: [VideoView] = [] + @IBOutlet weak var Container: AGEVideoContainer! + + /** + --- Microphones Picker --- + */ + @IBOutlet weak var selectMicsPicker: Picker! + var mics: [AgoraRtcDeviceInfo] = [] { + didSet { + DispatchQueue.main.async {[unowned self] in + self.selectMicsPicker.picker.addItems(withTitles: self.mics.map {$0.deviceName ?? "unknown"}) + } + } + } + var selectedMicrophone: AgoraRtcDeviceInfo? { + let index = self.selectMicsPicker.indexOfSelectedItem + if index >= 0 && index < mics.count { + return mics[index] + } else { + return nil + } + } + func initSelectMicsPicker() { + selectMicsPicker.label.stringValue = "Microphone".localized + // find device in a separate thread to avoid blocking main thread + let queue = DispatchQueue(label: "device.enumerateDevices") + queue.async {[unowned self] in + self.mics = self.agoraKit.enumerateDevices(.audioRecording) ?? [] + } + + selectMicsPicker.onSelectChanged { + if !self.isJoined { + return + } + // use selected devices + guard let micId = self.selectedMicrophone?.deviceId else { + return + } + self.agoraKit.setDevice(.audioRecording, deviceId: micId) + } + } + + /** + --- Layout Picker --- + */ + @IBOutlet weak var selectLayoutPicker: Picker! + let layouts = [Layout("1v1", 2), Layout("1v3", 4), Layout("1v8", 9), Layout("1v15", 16)] + var selectedLayout: Layout? { + let index = self.selectLayoutPicker.indexOfSelectedItem + if index >= 0 && index < layouts.count { + return layouts[index] + } else { + return nil + } + } + func initSelectLayoutPicker() { + layoutVideos(2) + selectLayoutPicker.label.stringValue = "Layout".localized + selectLayoutPicker.picker.addItems(withTitles: layouts.map { $0.label }) + selectLayoutPicker.onSelectChanged { + if self.isJoined { + return + } + guard let layout = self.selectedLayout else { return } + self.layoutVideos(layout.value) + } + } + + /** + --- Channel TextField --- + */ + @IBOutlet weak var channelField: Input! + func initChannelField() { + channelField.label.stringValue = "Channel".localized + channelField.field.placeholderString = "Channel Name".localized + } + + /** + --- Button --- + */ + @IBOutlet weak var joinChannelButton: NSButton! + func initJoinChannelButton() { + joinChannelButton.title = isJoined ? "Leave Channel".localized : "Join Channel".localized + } + + // indicate if current instance has joined channel + var isJoined: Bool = false { + didSet { + channelField.isEnabled = !isJoined + selectLayoutPicker.isEnabled = !isJoined + initJoinChannelButton() + } + } + + // indicate for doing something + var isProcessing: Bool = false { + didSet { + joinChannelButton.isEnabled = !isProcessing + } + } + + override func viewDidLoad() { + super.viewDidLoad() + // Do view setup here. + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + agoraKit.enableAudio() + + initSelectMicsPicker() + initSelectLayoutPicker() + initChannelField() + initJoinChannelButton() + } + + override func viewWillBeRemovedFromSplitView() { + if isJoined { + self.exAudio.stopWork() + agoraKit.leaveChannel { (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + } + } + } + + @IBAction func onJoinPressed(_ sender:Any) { + if !isJoined { + // check configuration + let sampleRate: UInt = 44100, audioChannel: UInt = 1 + let channel = channelField.stringValue + if channel.isEmpty { + return + } + guard let micId = selectedMicrophone?.deviceId else { + return + } + + agoraKit.setDevice(.audioRecording, deviceId: micId) + // disable video module in audio scene + agoraKit.disableVideo() + + // set live broadcaster mode + agoraKit.setChannelProfile(.liveBroadcasting) + // set myself as broadcaster to stream audio + agoraKit.setClientRole(.broadcaster) + + // setup external audio source + exAudio.setupExternalAudio(withAgoraKit: agoraKit, sampleRate: UInt32(sampleRate), channels: UInt32(audioChannel), audioCRMode: .sdkCaptureExterRender, ioType: .remoteIO) + // important!! this example is using onPlaybackAudioFrame to do custom rendering + // by default the audio output will still be processed by SDK hence below api call is mandatory to disable that behavior + agoraKit.setParameters("{\"che.audio.external_render\": false}") + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + isProcessing = true + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channel, info: nil, uid: 0, options: option) + if result != 0 { + self.isProcessing = false + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } else { + isProcessing = true + agoraKit.leaveChannel { (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + self.isProcessing = false + self.videos[0].uid = nil + self.isJoined = false + self.videos.forEach { + $0.uid = nil + $0.statsLabel.stringValue = "" + } + } + } + } + + func layoutVideos(_ count: Int) { + videos = [] + for i in 0...count - 1 { + let view = VideoView.createFromNib()! + if(i == 0) { + view.placeholder.stringValue = "Local" + } else { + view.placeholder.stringValue = "Remote \(i)" + } + videos.append(view) + } + // layout render view + Container.layoutStream(views: videos) + } +} + +/// agora rtc engine delegate events +extension CustomAudioRender: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.rawValue) occur") + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isProcessing = false + isJoined = true + let localVideo = videos[0] + localVideo.uid = uid + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + exAudio.startWork() + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // find a VideoView w/o uid assigned + if let remoteVideo = videos.first(where: { $0.uid == nil }) { + remoteVideo.uid = uid + } else { + LogUtils.log(message: "no video canvas available for \(uid), cancel bind", level: .warning) + } + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + if let remoteVideo = videos.first(where: { $0.uid == uid }) { + remoteVideo.uid = nil + } else { + LogUtils.log(message: "no matching video canvas for \(uid), cancel unbind", level: .warning) + } + } +} diff --git a/macOS/APIExample/Examples/Advanced/CustomAudioRender/zh-Hans.lproj/CustomAudioRender.strings b/macOS/APIExample/Examples/Advanced/CustomAudioRender/zh-Hans.lproj/CustomAudioRender.strings new file mode 100644 index 000000000..cc804167c --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/CustomAudioRender/zh-Hans.lproj/CustomAudioRender.strings @@ -0,0 +1,24 @@ + +/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "0pq-4D-qgt"; */ +"0pq-4D-qgt.title" = "1V8"; + +/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "J5P-DD-2lM"; */ +"J5P-DD-2lM.title" = "1V1"; + +/* Class = "NSButtonCell"; title = "Join"; ObjectID = "LpP-rx-fDz"; */ +"LpP-rx-fDz.title" = "加入频道"; + +/* Class = "NSMenuItem"; title = "1V3"; ObjectID = "Q9k-KS-Bb9"; */ +"Q9k-KS-Bb9.title" = "1V3"; + +/* Class = "NSViewController"; title = "Custom Audio Render"; ObjectID = "rPb-ur-msx"; */ +"rPb-ur-msx.title" = "音频自渲染"; + +/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "sav-ba-mHX"; */ +"sav-ba-mHX.title" = "离开频道"; + +/* Class = "NSTextFieldCell"; placeholderString = "加入频道"; ObjectID = "uZ0-mF-1r9"; */ +"uZ0-mF-1r9.placeholderString" = "输入频道名"; + +/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "zRn-Ca-xYL"; */ +"zRn-Ca-xYL.title" = "1V15"; diff --git a/macOS/APIExample/Examples/Advanced/CustomAudioSource/Base.lproj/CustomAudioSource.storyboard b/macOS/APIExample/Examples/Advanced/CustomAudioSource/Base.lproj/CustomAudioSource.storyboard new file mode 100644 index 000000000..65dd29c32 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/CustomAudioSource/Base.lproj/CustomAudioSource.storyboard @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Examples/Advanced/CustomAudioSource/CustomAudioSource.swift b/macOS/APIExample/Examples/Advanced/CustomAudioSource/CustomAudioSource.swift new file mode 100644 index 000000000..41cc85c39 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/CustomAudioSource/CustomAudioSource.swift @@ -0,0 +1,280 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import Cocoa +import AgoraRtcKit +import AGEVideoLayout + +class CustomAudioSource: BaseViewController { + var agoraKit: AgoraRtcEngineKit! + var exAudio: ExternalAudio = ExternalAudio.shared() + + var videos: [VideoView] = [] + @IBOutlet weak var Container: AGEVideoContainer! + + /** + --- Microphones Picker --- + */ + @IBOutlet weak var selectMicsPicker: Picker! + var mics: [AgoraRtcDeviceInfo] = [] { + didSet { + DispatchQueue.main.async {[unowned self] in + self.selectMicsPicker.picker.addItems(withTitles: self.mics.map {$0.deviceName ?? "unknown"}) + } + } + } + var selectedMicrophone: AgoraRtcDeviceInfo? { + let index = self.selectMicsPicker.indexOfSelectedItem + if index >= 0 && index < mics.count { + return mics[index] + } else { + return nil + } + } + func initSelectMicsPicker() { + selectMicsPicker.label.stringValue = "Microphone".localized + // find device in a separate thread to avoid blocking main thread + let queue = DispatchQueue(label: "device.enumerateDevices") + queue.async {[unowned self] in + self.mics = self.agoraKit.enumerateDevices(.audioRecording) ?? [] + } + + selectMicsPicker.onSelectChanged { + if !self.isJoined { + return + } + // use selected devices + guard let micId = self.selectedMicrophone?.deviceId else { + return + } + self.agoraKit.setDevice(.audioRecording, deviceId: micId) + } + } + + /** + --- Layout Picker --- + */ + @IBOutlet weak var selectLayoutPicker: Picker! + let layouts = [Layout("1v1", 2), Layout("1v3", 4), Layout("1v8", 9), Layout("1v15", 16)] + var selectedLayout: Layout? { + let index = self.selectLayoutPicker.indexOfSelectedItem + if index >= 0 && index < layouts.count { + return layouts[index] + } else { + return nil + } + } + func initSelectLayoutPicker() { + layoutVideos(2) + selectLayoutPicker.label.stringValue = "Layout".localized + selectLayoutPicker.picker.addItems(withTitles: layouts.map { $0.label }) + selectLayoutPicker.onSelectChanged { + if self.isJoined { + return + } + guard let layout = self.selectedLayout else { return } + self.layoutVideos(layout.value) + } + } + + /** + --- Channel TextField --- + */ + @IBOutlet weak var channelField: Input! + func initChannelField() { + channelField.label.stringValue = "Channel".localized + channelField.field.placeholderString = "Channel Name".localized + } + + /** + --- Button --- + */ + @IBOutlet weak var joinChannelButton: NSButton! + func initJoinChannelButton() { + joinChannelButton.title = isJoined ? "Leave Channel".localized : "Join Channel".localized + } + + // indicate if current instance has joined channel + var isJoined: Bool = false { + didSet { + channelField.isEnabled = !isJoined + selectLayoutPicker.isEnabled = !isJoined + initJoinChannelButton() + } + } + + // indicate for doing something + var isProcessing: Bool = false { + didSet { + joinChannelButton.isEnabled = !isProcessing + } + } + + override func viewDidLoad() { + super.viewDidLoad() + // Do view setup here. + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + agoraKit.enableAudio() + + initSelectMicsPicker() + initSelectLayoutPicker() + initChannelField() + initJoinChannelButton() + } + + override func viewWillBeRemovedFromSplitView() { + if isJoined { + self.exAudio.stopWork() + agoraKit.leaveChannel { (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + } + } + } + + @IBAction func onJoinPressed(_ sender:Any) { + if !isJoined { + // check configuration + let sampleRate: UInt = 44100, audioChannel: UInt = 1 + let channel = channelField.stringValue + if channel.isEmpty { + return + } + guard let micId = selectedMicrophone?.deviceId else { + return + } + + agoraKit.setDevice(.audioRecording, deviceId: micId) + // disable video module in audio scene + agoraKit.disableVideo() + + // set live broadcaster mode + agoraKit.setChannelProfile(.liveBroadcasting) + // set myself as broadcaster to stream audio + agoraKit.setClientRole(.broadcaster) + + // setup external audio source + exAudio.setupExternalAudio(withAgoraKit: agoraKit, sampleRate: UInt32(sampleRate), channels: UInt32(audioChannel), audioCRMode: .exterCaptureSDKRender, ioType: .remoteIO) + agoraKit.enableExternalAudioSource(withSampleRate: sampleRate, channelsPerFrame: audioChannel) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + isProcessing = true + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channel, info: nil, uid: 0, options: option) + if result != 0 { + self.isProcessing = false + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } else { + isProcessing = true + agoraKit.leaveChannel { (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + self.isProcessing = false + self.videos[0].uid = nil + self.isJoined = false + self.videos.forEach { + $0.uid = nil + $0.statsLabel.stringValue = "" + } + } + } + } + + func layoutVideos(_ count: Int) { + videos = [] + for i in 0...count - 1 { + let view = VideoView.createFromNib()! + if(i == 0) { + view.placeholder.stringValue = "Local" + } else { + view.placeholder.stringValue = "Remote \(i)" + } + videos.append(view) + } + // layout render view + Container.layoutStream(views: videos) + } +} + +/// agora rtc engine delegate events +extension CustomAudioSource: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.rawValue) occur") + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isProcessing = false + isJoined = true + let localVideo = videos[0] + localVideo.uid = uid + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + exAudio.startWork() + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // find a VideoView w/o uid assigned + if let remoteVideo = videos.first(where: { $0.uid == nil }) { + remoteVideo.uid = uid + } else { + LogUtils.log(message: "no video canvas available for \(uid), cancel bind", level: .warning) + } + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + if let remoteVideo = videos.first(where: { $0.uid == uid }) { + remoteVideo.uid = nil + } else { + LogUtils.log(message: "no matching video canvas for \(uid), cancel unbind", level: .warning) + } + } +} diff --git a/macOS/APIExample/Examples/Advanced/CustomAudioSource/zh-Hans.lproj/CustomAudioSource.strings b/macOS/APIExample/Examples/Advanced/CustomAudioSource/zh-Hans.lproj/CustomAudioSource.strings new file mode 100644 index 000000000..2afd2d317 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/CustomAudioSource/zh-Hans.lproj/CustomAudioSource.strings @@ -0,0 +1,24 @@ + +/* Class = "NSMenuItem"; title = "1V3"; ObjectID = "5Bj-Be-5dr"; */ +"5Bj-Be-5dr.title" = "1V3"; + +/* Class = "NSViewController"; title = "Custom Audio Source"; ObjectID = "8Q5-xy-D8A"; */ +"8Q5-xy-D8A.title" = "音频自采集"; + +/* Class = "NSTextFieldCell"; placeholderString = "加入频道"; ObjectID = "9hh-5D-rEK"; */ +"9hh-5D-rEK.placeholderString" = "输入频道名"; + +/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "CkQ-CH-Xcd"; */ +"CkQ-CH-Xcd.title" = "1V8"; + +/* Class = "NSButtonCell"; title = "Join"; ObjectID = "MCC-IO-OYe"; */ +"MCC-IO-OYe.title" = "加入频道"; + +/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "Wpu-17-eWW"; */ +"Wpu-17-eWW.title" = "1V15"; + +/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "dNt-Gv-ohJ"; */ +"dNt-Gv-ohJ.title" = "离开频道"; + +/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "yKw-5m-DrZ"; */ +"yKw-5m-DrZ.title" = "1V1"; diff --git a/macOS/APIExample/Examples/Advanced/CustomVideoRender/Base.lproj/CustomVideoRender.storyboard b/macOS/APIExample/Examples/Advanced/CustomVideoRender/Base.lproj/CustomVideoRender.storyboard new file mode 100644 index 000000000..d7e039b66 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/CustomVideoRender/Base.lproj/CustomVideoRender.storyboard @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Examples/Advanced/CustomVideoRender/CustomVideoRender.swift b/macOS/APIExample/Examples/Advanced/CustomVideoRender/CustomVideoRender.swift new file mode 100644 index 000000000..80288375e --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/CustomVideoRender/CustomVideoRender.swift @@ -0,0 +1,324 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import Cocoa +import AgoraRtcKit +import AGEVideoLayout + +class CustomVideoRender: BaseViewController { + var videos: [MetalVideoView] = [] + + @IBOutlet weak var Container: AGEVideoContainer! + + fileprivate let customCamera = AgoraCameraSourceMediaIO() + + var agoraKit: AgoraRtcEngineKit! + + /** + --- Resolutions Picker --- + */ + @IBOutlet weak var selectResolutionPicker: Picker! + var selectedResolution: Resolution? { + let index = self.selectResolutionPicker.indexOfSelectedItem + if index >= 0 && index < Configs.Resolutions.count { + return Configs.Resolutions[index] + } else { + return nil + } + } + func initSelectResolutionPicker() { + selectResolutionPicker.label.stringValue = "Resolution".localized + selectResolutionPicker.picker.addItems(withTitles: Configs.Resolutions.map { $0.name() }) + selectResolutionPicker.picker.selectItem(at: GlobalSettings.shared.resolutionSetting.selectedOption().value) + + selectResolutionPicker.onSelectChanged { + if !self.isJoined { + return + } + + guard let resolution = self.selectedResolution, + let fps = self.selectedFps else { + return + } + self.agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + } + } + + /** + --- Fps Picker --- + */ + @IBOutlet weak var selectFpsPicker: Picker! + var selectedFps: Int? { + let index = self.selectFpsPicker.indexOfSelectedItem + if index >= 0 && index < Configs.Fps.count { + return Configs.Fps[index] + } else { + return nil + } + } + func initSelectFpsPicker() { + selectFpsPicker.label.stringValue = "Frame Rate".localized + selectFpsPicker.picker.addItems(withTitles: Configs.Fps.map { "\($0)fps" }) + selectFpsPicker.picker.selectItem(at: GlobalSettings.shared.fpsSetting.selectedOption().value) + + selectFpsPicker.onSelectChanged { + if !self.isJoined { + return + } + + guard let resolution = self.selectedResolution, + let fps = self.selectedFps else { + return + } + self.agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + } + } + + /** + --- Layout Picker --- + */ + @IBOutlet weak var selectLayoutPicker: Picker! + let layouts = [Layout("1v1", 2), Layout("1v3", 4), Layout("1v8", 9), Layout("1v15", 16)] + var selectedLayout: Layout? { + let index = self.selectLayoutPicker.indexOfSelectedItem + if index >= 0 && index < layouts.count { + return layouts[index] + } else { + return nil + } + } + func initSelectLayoutPicker() { + layoutVideos(2) + selectLayoutPicker.label.stringValue = "Layout".localized + selectLayoutPicker.picker.addItems(withTitles: layouts.map { $0.label }) + selectLayoutPicker.onSelectChanged { + if self.isJoined { + return + } + guard let layout = self.selectedLayout else { return } + self.layoutVideos(layout.value) + } + } + + /** + --- Channel TextField --- + */ + @IBOutlet weak var channelField: Input! + func initChannelField() { + channelField.label.stringValue = "Channel".localized + channelField.field.placeholderString = "Channel Name".localized + } + + /** + --- Button --- + */ + @IBOutlet weak var joinChannelButton: NSButton! + func initJoinChannelButton() { + joinChannelButton.title = isJoined ? "Leave Channel".localized : "Join Channel".localized + } + + // indicate if current instance has joined channel + var isJoined: Bool = false { + didSet { + channelField.isEnabled = !isJoined + selectLayoutPicker.isEnabled = !isJoined + initJoinChannelButton() + } + } + + // indicate for doing something + var isProcessing: Bool = false { + didSet { + joinChannelButton.isEnabled = !isProcessing + } + } + + override func viewDidLoad() { + super.viewDidLoad() + // Do view setup here. + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + agoraKit.enableVideo() + + initSelectResolutionPicker() + initSelectFpsPicker() + initSelectLayoutPicker() + initChannelField() + initJoinChannelButton() + } + + override func viewWillBeRemovedFromSplitView() { + if isJoined { + agoraKit.leaveChannel { (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + } + } + } + + @IBAction func onJoinPressed(_ sender:Any) { + if !isJoined { + // check configuration + let channel = channelField.stringValue + if channel.isEmpty { + return + } + guard let resolution = selectedResolution, + let fps = selectedFps else { + return + } + + // set live broadcaster mode + agoraKit.setChannelProfile(.liveBroadcasting) + // set myself as broadcaster to stream video/audio + agoraKit.setClientRole(.broadcaster) + + // setup my own camera as custom video source + agoraKit.setVideoSource(customCamera) + // enable video module and set up video encoding configs + agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + + // set up your own render + agoraKit.setLocalVideoRenderer(videos[0].videocanvas) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + isProcessing = true + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channel, info: nil, uid: 0, options: option) + if result != 0 { + isProcessing = false + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } else { + isProcessing = true + agoraKit.leaveChannel { (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + self.isProcessing = false + self.videos[0].uid = nil + self.isJoined = false + self.videos.forEach { + $0.uid = nil + } + } + } + } + + func layoutVideos(_ count: Int) { + videos = [] + for i in 0...count - 1 { + let view = MetalVideoView.createFromNib()! + if(i == 0) { + view.placeholder.stringValue = "Local" + } else { + view.placeholder.stringValue = "Remote \(i)" + } + videos.append(view) + } + // layout render view + Container.layoutStream(views: videos) + } +} + +/// agora rtc engine delegate events +extension CustomVideoRender: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.rawValue) occur") + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isProcessing = false + isJoined = true + let localVideo = videos[0] + localVideo.uid = uid + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // find a VideoView w/o uid assigned + if let remoteVideo = videos.first(where: { $0.uid == nil }) { + remoteVideo.uid = uid + agoraKit.setRemoteVideoRenderer(remoteVideo.videocanvas, forUserId: uid) + } else { + LogUtils.log(message: "no video canvas available for \(uid), cancel bind", level: .warning) + } + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + if let remoteVideo = videos.first(where: { $0.uid == uid }) { + remoteVideo.uid = nil + agoraKit.setRemoteVideoRenderer(nil, forUserId: uid) + } else { + LogUtils.log(message: "no matching video canvas for \(uid), cancel unbind", level: .warning) + } + } +} diff --git a/macOS/APIExample/Examples/Advanced/CustomVideoRender/zh-Hans.lproj/CustomVideoRender.strings b/macOS/APIExample/Examples/Advanced/CustomVideoRender/zh-Hans.lproj/CustomVideoRender.strings new file mode 100644 index 000000000..b8c1e92f7 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/CustomVideoRender/zh-Hans.lproj/CustomVideoRender.strings @@ -0,0 +1,24 @@ + +/* Class = "NSButtonCell"; title = "Join"; ObjectID = "4f5-cK-Lrg"; */ +"4f5-cK-Lrg.title" = "加入频道"; + +/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "8JX-YX-iAW"; */ +"8JX-YX-iAW.title" = "1V15"; + +/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "PpQ-ki-MC0"; */ +"PpQ-ki-MC0.title" = "1V8"; + +/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "hzs-Vp-M59"; */ +"hzs-Vp-M59.title" = "1V1"; + +/* Class = "NSViewController"; title = "Custom Video Source(MediaIO)"; ObjectID = "jEL-F4-BwV"; */ +"jEL-F4-BwV.title" = "音频自渲染"; + +/* Class = "NSMenuItem"; title = "1V3"; ObjectID = "q4U-yg-aWx"; */ +"q4U-yg-aWx.title" = "1V3"; + +/* Class = "NSTextFieldCell"; placeholderString = "加入频道"; ObjectID = "xtu-Fh-nL8"; */ +"xtu-Fh-nL8.placeholderString" = "输入频道名"; + +/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "z6I-ve-sPC"; */ +"z6I-ve-sPC.title" = "离开频道"; diff --git a/macOS/APIExample/Examples/Advanced/CustomVideoSourceMediaIO/Base.lproj/CustomVideoSourceMediaIO.storyboard b/macOS/APIExample/Examples/Advanced/CustomVideoSourceMediaIO/Base.lproj/CustomVideoSourceMediaIO.storyboard new file mode 100644 index 000000000..7ffa6c5d9 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/CustomVideoSourceMediaIO/Base.lproj/CustomVideoSourceMediaIO.storyboard @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Examples/Advanced/CustomVideoSourceMediaIO/CustomVideoSourceMediaIO.swift b/macOS/APIExample/Examples/Advanced/CustomVideoSourceMediaIO/CustomVideoSourceMediaIO.swift new file mode 100644 index 000000000..7365a13a8 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/CustomVideoSourceMediaIO/CustomVideoSourceMediaIO.swift @@ -0,0 +1,341 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import Cocoa +import AgoraRtcKit +import AGEVideoLayout + +class CustomVideoSourceMediaIO: BaseViewController { + var videos: [VideoView] = [] + + fileprivate let customCamera = AgoraCameraSourceMediaIO() + + var agoraKit: AgoraRtcEngineKit! + + @IBOutlet weak var Container: AGEVideoContainer! + + /** + --- Resolutions Picker --- + */ + @IBOutlet weak var selectResolutionPicker: Picker! + var selectedResolution: Resolution? { + let index = self.selectResolutionPicker.indexOfSelectedItem + if index >= 0 && index < Configs.Resolutions.count { + return Configs.Resolutions[index] + } else { + return nil + } + } + func initSelectResolutionPicker() { + selectResolutionPicker.label.stringValue = "Resolution".localized + selectResolutionPicker.picker.addItems(withTitles: Configs.Resolutions.map { $0.name() }) + selectResolutionPicker.picker.selectItem(at: GlobalSettings.shared.resolutionSetting.selectedOption().value) + + selectResolutionPicker.onSelectChanged { + if !self.isJoined { + return + } + + guard let resolution = self.selectedResolution, + let fps = self.selectedFps else { + return + } + self.agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + } + } + + /** + --- Fps Picker --- + */ + @IBOutlet weak var selectFpsPicker: Picker! + var selectedFps: Int? { + let index = self.selectFpsPicker.indexOfSelectedItem + if index >= 0 && index < Configs.Fps.count { + return Configs.Fps[index] + } else { + return nil + } + } + func initSelectFpsPicker() { + selectFpsPicker.label.stringValue = "Frame Rate".localized + selectFpsPicker.picker.addItems(withTitles: Configs.Fps.map { "\($0)fps" }) + selectFpsPicker.picker.selectItem(at: GlobalSettings.shared.fpsSetting.selectedOption().value) + + selectFpsPicker.onSelectChanged { + if !self.isJoined { + return + } + guard let resolution = self.selectedResolution, + let fps = self.selectedFps else { + return + } + self.agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + } + } + + /** + --- Layout Picker --- + */ + @IBOutlet weak var selectLayoutPicker: Picker! + let layouts = [Layout("1v1", 2), Layout("1v3", 4), Layout("1v8", 9), Layout("1v15", 16)] + var selectedLayout: Layout? { + let index = self.selectLayoutPicker.indexOfSelectedItem + if index >= 0 && index < layouts.count { + return layouts[index] + } else { + return nil + } + } + func initSelectLayoutPicker() { + layoutVideos(2) + selectLayoutPicker.label.stringValue = "Layout".localized + selectLayoutPicker.picker.addItems(withTitles: layouts.map { $0.label }) + selectLayoutPicker.onSelectChanged { + if self.isJoined { + return + } + guard let layout = self.selectedLayout else { return } + self.layoutVideos(layout.value) + } + } + + /** + --- Channel TextField --- + */ + @IBOutlet weak var channelField: Input! + func initChannelField() { + channelField.label.stringValue = "Channel".localized + channelField.field.placeholderString = "Channel Name".localized + } + + /** + --- Button --- + */ + @IBOutlet weak var joinChannelButton: NSButton! + func initJoinChannelButton() { + joinChannelButton.title = isJoined ? "Leave Channel".localized : "Join Channel".localized + } + + // indicate if current instance has joined channel + var isJoined: Bool = false { + didSet { + channelField.isEnabled = !isJoined + selectLayoutPicker.isEnabled = !isJoined + initJoinChannelButton() + } + } + + // indicate for doing something + var isProcessing: Bool = false { + didSet { + joinChannelButton.isEnabled = !isProcessing + } + } + + override func viewDidLoad() { + super.viewDidLoad() + // Do view setup here. + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + agoraKit.enableVideo() + + initSelectResolutionPicker() + initSelectFpsPicker() + initSelectLayoutPicker() + initChannelField() + initJoinChannelButton() + } + + override func viewWillBeRemovedFromSplitView() { + if isJoined { + agoraKit.leaveChannel { (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + } + } + } + + @IBAction func onJoinPressed(_ sender:Any) { + if !isJoined { + // check configuration + let channel = channelField.stringValue + if channel.isEmpty { + return + } + guard let resolution = selectedResolution, + let fps = selectedFps else { + return + } + // set live broadcaster mode + agoraKit.setChannelProfile(.liveBroadcasting) + // set myself as broadcaster to stream video/audio + agoraKit.setClientRole(.broadcaster) + // setup my own camera as custom video source + agoraKit.setVideoSource(customCamera) + // enable video module and set up video encoding configs + agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + // set up local video to render your local camera preview + let localVideo = videos[0] + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + // the view to be binded + videoCanvas.view = localVideo.videocanvas + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + isProcessing = true + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channel, info: nil, uid: 0, options: option) + if result != 0 { + isProcessing = false + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } else { + isProcessing = true + agoraKit.leaveChannel { [unowned self] (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + self.isProcessing = false + self.videos[0].uid = nil + self.videos[1].uid = nil + self.isJoined = false + self.videos.forEach { + $0.uid = nil + $0.statsLabel.stringValue = "" + } + } + } + } + + func layoutVideos(_ count: Int) { + videos = [] + for i in 0...count - 1 { + let view = VideoView.createFromNib()! + if(i == 0) { + view.placeholder.stringValue = "Local" + } else { + view.placeholder.stringValue = "Remote \(i)" + } + videos.append(view) + } + // layout render view + Container.layoutStream(views: videos) + } +} + +/// agora rtc engine delegate events +extension CustomVideoSourceMediaIO: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + if isProcessing { + isProcessing = false + } + self.showAlert(title: "Error", message: "Error \(errorCode.rawValue) occur") + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isProcessing = false + isJoined = true + let localVideo = videos[0] + localVideo.uid = uid + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // find a VideoView w/o uid assigned + if let remoteVideo = videos.first(where: { $0.uid == nil }) { + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videocanvas + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + remoteVideo.uid = uid + } else { + LogUtils.log(message: "no video canvas available for \(uid), cancel bind", level: .warning) + } + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + if let remoteVideo = videos.first(where: { $0.uid == uid }) { + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + remoteVideo.uid = nil + } else { + LogUtils.log(message: "no matching video canvas for \(uid), cancel unbind", level: .warning) + } + } +} diff --git a/macOS/APIExample/Examples/Advanced/CustomVideoSourceMediaIO/zh-Hans.lproj/CustomVideoSourceMediaIO.strings b/macOS/APIExample/Examples/Advanced/CustomVideoSourceMediaIO/zh-Hans.lproj/CustomVideoSourceMediaIO.strings new file mode 100644 index 000000000..d065af902 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/CustomVideoSourceMediaIO/zh-Hans.lproj/CustomVideoSourceMediaIO.strings @@ -0,0 +1,24 @@ + +/* Class = "NSButtonCell"; title = "Join"; ObjectID = "1ik-om-mWj"; */ +"1ik-om-mWj.title" = "加入频道"; + +/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "6f9-0B-egB"; */ +"6f9-0B-egB.title" = "1V1"; + +/* Class = "NSViewController"; title = "Custom Video Source(MediaIO)"; ObjectID = "Gwp-vd-c2J"; */ +"Gwp-vd-c2J.title" = "音频自采集(MediaIO)"; + +/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "Owt-vb-7U9"; */ +"Owt-vb-7U9.title" = "离开频道"; + +/* Class = "NSMenuItem"; title = "1V3"; ObjectID = "S4i-eh-YzK"; */ +"S4i-eh-YzK.title" = "1V3"; + +/* Class = "NSTextFieldCell"; placeholderString = "加入频道"; ObjectID = "aj5-Fn-je9"; */ +"aj5-Fn-je9.placeholderString" = "输入频道名"; + +/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "cxo-X2-S8L"; */ +"cxo-X2-S8L.title" = "1V15"; + +/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "zu1-vg-leG"; */ +"zu1-vg-leG.title" = "1V8"; diff --git a/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/Base.lproj/CustomVideoSourcePush.storyboard b/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/Base.lproj/CustomVideoSourcePush.storyboard new file mode 100644 index 000000000..1708e7236 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/Base.lproj/CustomVideoSourcePush.storyboard @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/CustomVideoSourcePush.swift b/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/CustomVideoSourcePush.swift new file mode 100644 index 000000000..dd00271b3 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/CustomVideoSourcePush.swift @@ -0,0 +1,351 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import Cocoa +import AgoraRtcKit +import AGEVideoLayout + +class CustomVideoSourcePush: BaseViewController { + var remoteVideos: [VideoView] = [] + + @IBOutlet weak var Container: AGEVideoContainer! + + var localPreview: CustomVideoSourcePreview? + + var allVideos: [NSView] = [] + + fileprivate var customCamera:AgoraCameraSourcePush? + + var agoraKit: AgoraRtcEngineKit! + + /** + --- Resolutions Picker --- + */ + @IBOutlet weak var selectResolutionPicker: Picker! + var selectedResolution: Resolution? { + let index = self.selectResolutionPicker.indexOfSelectedItem + if index >= 0 && index < Configs.Resolutions.count { + return Configs.Resolutions[index] + } else { + return nil + } + } + func initSelectResolutionPicker() { + selectResolutionPicker.label.stringValue = "Resolution".localized + selectResolutionPicker.picker.addItems(withTitles: Configs.Resolutions.map { $0.name() }) + selectResolutionPicker.picker.selectItem(at: GlobalSettings.shared.resolutionSetting.selectedOption().value) + + selectResolutionPicker.onSelectChanged { + if !self.isJoined { + return + } + + guard let resolution = self.selectedResolution, + let fps = self.selectedFps else { + return + } + self.agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + } + } + + /** + --- Fps Picker --- + */ + @IBOutlet weak var selectFpsPicker: Picker! + var selectedFps: Int? { + let index = self.selectFpsPicker.indexOfSelectedItem + if index >= 0 && index < Configs.Fps.count { + return Configs.Fps[index] + } else { + return nil + } + } + func initSelectFpsPicker() { + selectFpsPicker.label.stringValue = "Frame Rate".localized + selectFpsPicker.picker.addItems(withTitles: Configs.Fps.map { "\($0)fps" }) + selectFpsPicker.picker.selectItem(at: GlobalSettings.shared.fpsSetting.selectedOption().value) + + selectFpsPicker.onSelectChanged { + if !self.isJoined { + return + } + + guard let resolution = self.selectedResolution, + let fps = self.selectedFps else { + return + } + self.agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + } + } + + /** + --- Layout Picker --- + */ + @IBOutlet weak var selectLayoutPicker: Picker! + let layouts = [Layout("1v1", 2), Layout("1v3", 4), Layout("1v8", 9), Layout("1v15", 16)] + var selectedLayout: Layout? { + let index = self.selectLayoutPicker.indexOfSelectedItem + if index >= 0 && index < layouts.count { + return layouts[index] + } else { + return nil + } + } + func initSelectLayoutPicker() { + layoutVideos(2) + selectLayoutPicker.label.stringValue = "Layout".localized + selectLayoutPicker.picker.addItems(withTitles: layouts.map { $0.label }) + selectLayoutPicker.onSelectChanged { + if self.isJoined { + return + } + guard let layout = self.selectedLayout else { return } + self.layoutVideos(layout.value) + } + } + + /** + --- Channel TextField --- + */ + @IBOutlet weak var channelField: Input! + func initChannelField() { + channelField.label.stringValue = "Channel".localized + channelField.field.placeholderString = "Channel Name".localized + } + + /** + --- Button --- + */ + @IBOutlet weak var joinChannelButton: NSButton! + func initJoinChannelButton() { + joinChannelButton.title = isJoined ? "Leave Channel".localized : "Join Channel".localized + } + + // indicate if current instance has joined channel + var isJoined: Bool = false { + didSet { + channelField.isEnabled = !isJoined + selectLayoutPicker.isEnabled = !isJoined + initJoinChannelButton() + } + } + + // indicate for doing something + var isProcessing: Bool = false { + didSet { + joinChannelButton.isEnabled = !isProcessing + } + } + + override func viewDidLoad() { + super.viewDidLoad() + // Do view setup here. + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + agoraKit.enableVideo() + + initSelectResolutionPicker() + initSelectFpsPicker() + initSelectLayoutPicker() + initChannelField() + initJoinChannelButton() + } + + override func viewWillBeRemovedFromSplitView() { + if isJoined { + self.customCamera?.stopCapture() + agoraKit.leaveChannel { (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + self.remoteVideos[0].uid = nil + } + } + } + + @IBAction func onJoinPressed(_ sender:Any) { + if !isJoined { + // check configuration + let channel = channelField.stringValue + if channel.isEmpty { + return + } + guard let resolution = selectedResolution, + let fps = selectedFps else { + return + } + + // set live broadcaster mode + agoraKit.setChannelProfile(.liveBroadcasting) + // set myself as broadcaster to stream video/audio + agoraKit.setClientRole(.broadcaster) + + // setup my own camera as custom video source + customCamera = AgoraCameraSourcePush(delegate: self, videoView: localPreview!) + agoraKit.setExternalVideoSource(true, useTexture: true, pushMode: true) + customCamera?.startCapture(ofCamera: .defaultCamera()) + // enable video module and set up video encoding configs + agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + isProcessing = true + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channel, info: nil, uid: 0, options: option) + if result != 0 { + isProcessing = false + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } else { + isProcessing = true + self.customCamera?.stopCapture() + agoraKit.leaveChannel { (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + self.isProcessing = false + self.isJoined = false + } + } + } + + func layoutVideos(_ count: Int) { + remoteVideos = [] + allVideos = [] + if localPreview == nil { + localPreview = CustomVideoSourcePreview(frame: .zero) + } + allVideos.append(localPreview!) + + for i in 0...count - 2 { + let view = VideoView.createFromNib()! + view.placeholder.stringValue = "Remote \(i)" + remoteVideos.append(view) + allVideos.append(view) + } + + // layout render view + Container.layoutStream(views: allVideos) + } +} + +/// agora rtc engine delegate events +extension CustomVideoSourcePush: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + if isProcessing { + isProcessing = false + } + self.showAlert(title: "Error", message: "Error \(errorCode.rawValue) occur") + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isProcessing = false + isJoined = true + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // find a VideoView w/o uid assigned + if let remoteVideo = remoteVideos.first(where: { $0.uid == nil }) { + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videocanvas + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + remoteVideo.uid = uid + } else { + LogUtils.log(message: "no video canvas available for \(uid), cancel bind", level: .warning) + } + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + if let remoteVideo = remoteVideos.first(where: { $0.uid == uid }) { + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + remoteVideo.uid = nil + } else { + LogUtils.log(message: "no matching video canvas for \(uid), cancel unbind", level: .warning) + } + } +} + +extension CustomVideoSourcePush: AgoraCameraSourcePushDelegate { + func myVideoCapture(_ capture: AgoraCameraSourcePush, didOutputSampleBuffer pixelBuffer: CVPixelBuffer, rotation: Int, timeStamp: CMTime) { + let videoFrame = AgoraVideoFrame() + videoFrame.format = 12 + videoFrame.time = timeStamp + videoFrame.textureBuf = pixelBuffer + videoFrame.rotation = 0 + agoraKit.pushExternalVideoFrame(videoFrame) + } +} diff --git a/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/zh-Hans.lproj/CustomVideoSourcePush.strings b/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/zh-Hans.lproj/CustomVideoSourcePush.strings new file mode 100644 index 000000000..ec3db92f9 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/CustomVideoSourcePush/zh-Hans.lproj/CustomVideoSourcePush.strings @@ -0,0 +1,24 @@ + +/* Class = "NSTextFieldCell"; placeholderString = "加入频道"; ObjectID = "KSj-Qd-L7B"; */ +"KSj-Qd-L7B.placeholderString" = "输入频道名"; + +/* Class = "NSButtonCell"; title = "Join"; ObjectID = "XQ9-2H-aV1"; */ +"XQ9-2H-aV1.title" = "加入频道"; + +/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "esh-Yv-lrq"; */ +"esh-Yv-lrq.title" = "离开频道"; + +/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "lxe-dD-iYs"; */ +"lxe-dD-iYs.title" = "1V1"; + +/* Class = "NSViewController"; title = "Custom Video Source (Push)"; ObjectID = "sXF-vm-Rrb"; */ +"sXF-vm-Rrb.title" = "音频自采集(Push)"; + +/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "tBU-fM-94k"; */ +"tBU-fM-94k.title" = "1V15"; + +/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "ukW-YV-Pc0"; */ +"ukW-YV-Pc0.title" = "1V8"; + +/* Class = "NSMenuItem"; title = "1V3"; ObjectID = "z6y-AQ-Yeq"; */ +"z6y-AQ-Yeq.title" = "1V3"; diff --git a/macOS/APIExample/Examples/Advanced/JoinMultiChannel/Base.lproj/JoinMultiChannel.storyboard b/macOS/APIExample/Examples/Advanced/JoinMultiChannel/Base.lproj/JoinMultiChannel.storyboard new file mode 100644 index 000000000..6c9ec1ab4 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/JoinMultiChannel/Base.lproj/JoinMultiChannel.storyboard @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Examples/Advanced/JoinMultiChannel/JoinMultiChannel.swift b/macOS/APIExample/Examples/Advanced/JoinMultiChannel/JoinMultiChannel.swift new file mode 100644 index 000000000..4fc3aa031 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/JoinMultiChannel/JoinMultiChannel.swift @@ -0,0 +1,366 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import Cocoa +import AgoraRtcKit +import AGEVideoLayout + +class JoinMultipleChannel: BaseViewController { + var videos: [VideoView] = [] + var videos2: [VideoView] = [] + + @IBOutlet weak var container: AGEVideoContainer! + @IBOutlet weak var container2: AGEVideoContainer! + + var channel1: AgoraRtcChannel? + var channel2: AgoraRtcChannel? + + var agoraKit: AgoraRtcEngineKit! + + // indicate if current instance has joined channel1 + var isJoined: Bool = false { + didSet { + channelField1.isEnabled = !isJoined + initJoinChannel1Button() + } + } + /** + --- Channel1 TextField --- + */ + @IBOutlet weak var channelField1: Input! + func initChannelField1() { + channelField1.label.stringValue = "Channel".localized + "1" + channelField1.field.placeholderString = "Channel Name".localized + "1" + } + /** + --- Join Channel1 Button --- + */ + @IBOutlet weak var joinChannel1Button: NSButton! + func initJoinChannel1Button() { + joinChannel1Button.title = isJoined ? "Leave Channel".localized : "Join Channel".localized + } + @IBAction func onJoinChannel1ButtonPressed(_ sender: NSButton) { + if !isJoined { + // auto subscribe options after join channel + let mediaOptions = AgoraRtcChannelMediaOptions() + mediaOptions.autoSubscribeAudio = true + mediaOptions.autoSubscribeVideo = true + + var channel: AgoraRtcChannel? + if channel1 == nil { + channel1 = agoraKit.createRtcChannel(channelField1.stringValue) + } + channel = channel1 + channel?.setRtcChannelDelegate(self) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let result = channel?.join(byToken: nil, info: nil, uid: 0, options: mediaOptions) ?? -1 + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel1 call failed: \(result), please check your params") + } + } else { + channel1?.leave() + if let channelName = channel1?.getId() { + if isPublished && channelName == selectedChannel { + if let channel = getChannelByName(selectedChannel) { + channel.setClientRole(.audience) + channel.unpublish() + isPublished = false + } + } + selectChannelsPicker.picker.removeItem(withTitle: channelName) + } + channel1?.destroy() + channel1 = nil + isJoined = false + } + } + + // indicate if current instance has joined channel2 + var isJoined2: Bool = false { + didSet { + channelField2.isEnabled = !isJoined2 + initJoinChannel2Button() + } + } + /** + --- Channel1 TextField --- + */ + @IBOutlet weak var channelField2: Input! + func initChannelField2() { + channelField2.label.stringValue = "Channel".localized + "2" + channelField2.field.placeholderString = "Channel Name".localized + "2" + } + /** + --- Join Channel1 Button --- + */ + @IBOutlet weak var joinChannel2Button: NSButton! + func initJoinChannel2Button() { + joinChannel2Button.title = isJoined2 ? "Leave Channel".localized : "Join Channel".localized + } + @IBAction func onJoinChannel2ButtonPressed(_ sender:NSButton) { + if !isJoined2 { + // auto subscribe options after join channel + let mediaOptions = AgoraRtcChannelMediaOptions() + mediaOptions.autoSubscribeAudio = true + mediaOptions.autoSubscribeVideo = true + + var channel: AgoraRtcChannel? + if channel2 == nil { + channel2 = agoraKit.createRtcChannel(channelField2.stringValue) + } + channel = channel2 + + channel?.setRtcChannelDelegate(self) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + let result = channel?.join(byToken: nil, info: nil, uid: 0, options: mediaOptions) ?? -1 + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel1 call failed: \(result), please check your params") + } + } else { + channel2?.leave() + if let channelName = channel2?.getId() { + if isPublished && channelName == selectedChannel { + if let channel = getChannelByName(selectedChannel) { + channel.setClientRole(.audience) + channel.unpublish() + isPublished = false + } + } + selectChannelsPicker.picker.removeItem(withTitle: channelName) + } + channel2?.destroy() + channel2 = nil + isJoined2 = false + } + } + + var isPublished: Bool = false { + didSet { + selectChannelsPicker.isEnabled = !isPublished + initPublishButton() + } + } + /** + --- Channels Picker --- + */ + @IBOutlet weak var selectChannelsPicker: Picker! + var selectedChannel: String? { + return selectChannelsPicker.picker.selectedItem?.title + } + func initSelectChannelsPicker() { + selectChannelsPicker.label.stringValue = "Channel".localized + } + /** + --- Publish Button --- + */ + @IBOutlet weak var publishButton: NSButton! + func initPublishButton() { + publishButton.title = isPublished ? "Unpublish".localized : "Publish".localized + } + @IBAction func onPublishPressed(_ sender: Any) { + if !isPublished { + if let channel = getChannelByName(selectedChannel) { + channel.setClientRole(.broadcaster) + channel.publish() + isPublished = true + } + } else { + if let channel = getChannelByName(selectedChannel) { + channel.setClientRole(.audience) + channel.unpublish() + isPublished = false + } + } + } + + override func viewDidLoad() { + super.viewDidLoad() + layoutVideos() + + // set up agora instance when view loaded + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + // this is mandatory to get camera list + agoraKit.enableVideo() + + initChannelField1() + initJoinChannel1Button() + initChannelField2() + initJoinChannel2Button() + initSelectChannelsPicker() + initPublishButton() + + // set live broadcaster mode + agoraKit.setChannelProfile(.liveBroadcasting) + + let resolution = Configs.Resolutions[GlobalSettings.shared.resolutionSetting.selectedOption().value] + let fps = Configs.Fps[GlobalSettings.shared.fpsSetting.selectedOption().value] + // enable video module and set up video encoding configs + agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + // set up local video to render your local camera preview + let localVideo = videos[0] + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + // the view to be binded + videoCanvas.view = localVideo.videocanvas + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + agoraKit.startPreview() + } + + override func viewWillBeRemovedFromSplitView() { + channel1?.leave() + channel1?.destroy() + channel2?.leave() + channel2?.destroy() + } + + func getChannelByName(_ channelName: String?) -> AgoraRtcChannel? { + if channel1?.getId() == channelName { + return channel1 + } else if channel2?.getId() == channelName { + return channel2 + } + return nil + } + + func layoutVideos() { + videos = [VideoView.createFromNib()!] + videos[0].placeholder.stringValue = "Local" + // layout render view + container.layoutStream(views: videos) + + videos2 = [VideoView.createFromNib()!, VideoView.createFromNib()!] + videos2[0].placeholder.stringValue = "Channel1\nRemote" + videos2[1].placeholder.stringValue = "Channel2\nRemote" + container2.layoutStream2(views: videos2) + } +} + +/// agora rtc engine delegate events +extension JoinMultipleChannel: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.rawValue) occur") + } +} + +extension JoinMultipleChannel: AgoraRtcChannelDelegate { + func rtcChannelDidJoin(_ rtcChannel: AgoraRtcChannel, withUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "Join \(rtcChannel.getId() ?? "") with uid \(uid) elapsed \(elapsed)ms", level: .info) + selectChannelsPicker.picker.addItem(withTitle: rtcChannel.getId()!) + if (channel1 == rtcChannel) { + isJoined = true + } else { + isJoined2 = true + } + } + /// callback when warning occured for a channel, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcChannel(_ rtcChannel: AgoraRtcChannel, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "channel: \(rtcChannel.getId() ?? ""), warning: \(warningCode.rawValue)", level: .warning) + } + + /// callback when error occured for a channel, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcChannel(_ rtcChannel: AgoraRtcChannel, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.rawValue) occur") + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcChannel(_ rtcChannel: AgoraRtcChannel, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // Only one remote video view is available for this + // tutorial. Here we check if there exists a surface + // view tagged as this uid. + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = channel1 == rtcChannel ? videos2[0].videocanvas : videos2[1].videocanvas + videoCanvas.renderMode = .hidden + // set channelId so that it knows which channel the video belongs to + videoCanvas.channelId = rtcChannel.getId() + agoraKit.setupRemoteVideo(videoCanvas) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcChannel(_ rtcChannel: AgoraRtcChannel, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + // set channelId so that it knows which channel the video belongs to + videoCanvas.channelId = rtcChannel.getId() + agoraKit.setupRemoteVideo(videoCanvas) + } +} diff --git a/macOS/APIExample/Examples/Advanced/JoinMultiChannel/zh-Hans.lproj/JoinMultiChannel.strings b/macOS/APIExample/Examples/Advanced/JoinMultiChannel/zh-Hans.lproj/JoinMultiChannel.strings new file mode 100644 index 000000000..f029b20d0 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/JoinMultiChannel/zh-Hans.lproj/JoinMultiChannel.strings @@ -0,0 +1,27 @@ + +/* Class = "NSTextFieldCell"; placeholderString = "Channel Name 2"; ObjectID = "Ab2-sI-Ld3"; */ +"Ab2-sI-Ld3.placeholderString" = "输入频道名2"; + +/* Class = "NSButtonCell"; title = "Unpublish"; ObjectID = "Hvn-10-7hC"; */ +"Hvn-10-7hC.title" = "停止发流"; + +/* Class = "NSViewController"; title = "Join Multiple Channels"; ObjectID = "IBJ-wZ-9Xx"; */ +"IBJ-wZ-9Xx.title" = "Join Multiple Channels"; + +/* Class = "NSButtonCell"; title = "Publish"; ObjectID = "Rau-85-whm"; */ +"Rau-85-whm.title" = "发流"; + +/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "Xtr-fU-GZ5"; */ +"Xtr-fU-GZ5.title" = "离开频道"; + +/* Class = "NSButtonCell"; title = "Join"; ObjectID = "Zjl-Vt-wOj"; */ +"Zjl-Vt-wOj.title" = "加入频道"; + +/* Class = "NSTextFieldCell"; placeholderString = "Channel Name 1"; ObjectID = "p0a-zy-yqS"; */ +"p0a-zy-yqS.placeholderString" = "输入频道名1"; + +/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "ttd-9y-14q"; */ +"ttd-9y-14q.title" = "离开频道"; + +/* Class = "NSButtonCell"; title = "Join"; ObjectID = "u6j-cJ-1Pe"; */ +"u6j-cJ-1Pe.title" = "加入频道"; diff --git a/macOS/APIExample/Examples/Advanced/PrecallTest/Base.lproj/PrecallTest.storyboard b/macOS/APIExample/Examples/Advanced/PrecallTest/Base.lproj/PrecallTest.storyboard new file mode 100644 index 000000000..5362eeae2 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/PrecallTest/Base.lproj/PrecallTest.storyboard @@ -0,0 +1,305 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Examples/Advanced/PrecallTest/PrecallTest.swift b/macOS/APIExample/Examples/Advanced/PrecallTest/PrecallTest.swift new file mode 100644 index 000000000..af62e24b7 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/PrecallTest/PrecallTest.swift @@ -0,0 +1,313 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import Cocoa +import AgoraRtcKit +import AGEVideoLayout + +class PrecallTest: BaseViewController { + var videos: [VideoView] = [] + var timer:Timer? + + @IBOutlet weak var cameraPicker: NSPopUpButton! + @IBOutlet weak var micPicker: NSPopUpButton! + @IBOutlet weak var speakerPicker: NSPopUpButton! + @IBOutlet weak var startCameraTestBtn: NSButton! + @IBOutlet weak var stopCameraTestBtn: NSButton! + @IBOutlet weak var startMicTestBtn: NSButton! + @IBOutlet weak var stopMicTestBtn: NSButton! + @IBOutlet weak var startSpeakerTestBtn: NSButton! + @IBOutlet weak var stopSpeakerTestBtn: NSButton! + @IBOutlet weak var startLoopbackTestBtn: NSButton! + @IBOutlet weak var stopLoopbackTestBtn: NSButton! + @IBOutlet weak var startLastmileTestBtn: NSButton! + @IBOutlet weak var lastmileResultLabel: NSTextField! + @IBOutlet weak var lastmileProbResultLabel: NSTextField! + @IBOutlet weak var lastmileActivityView: NSProgressIndicator! + @IBOutlet weak var micTestingVolumeIndicator: NSProgressIndicator! + @IBOutlet weak var echoTestCountDownLabel: NSTextField! + @IBOutlet weak var echoTestPopover: NSView! + @IBOutlet weak var echoValidateCountDownLabel: NSTextField! + @IBOutlet weak var echoValidatePopover: NSView! + var cameras:[AgoraRtcDeviceInfo] = [] { + didSet { + DispatchQueue.main.async {[unowned self] in + self.cameraPicker.addItems(withTitles: self.cameras.map({ (device: AgoraRtcDeviceInfo) -> String in + return (device.deviceName ?? "") + })) + } + } + } + var mics:[AgoraRtcDeviceInfo] = [] { + didSet { + DispatchQueue.main.async {[unowned self] in + self.micPicker.addItems(withTitles: self.mics.map({ (device: AgoraRtcDeviceInfo) -> String in + return (device.deviceName ?? "") + })) + } + } + } + var speakers:[AgoraRtcDeviceInfo] = [] { + didSet { + DispatchQueue.main.async {[unowned self] in + self.speakerPicker.addItems(withTitles: self.speakers.map({ (device: AgoraRtcDeviceInfo) -> String in + return (device.deviceName ?? "") + })) + } + } + } + + // indicate if camera testing is going on + var isTestingCamera: Bool = false { + didSet { + startCameraTestBtn.isHidden = isTestingCamera + stopCameraTestBtn.isHidden = !isTestingCamera + } + } + + // indicate if mic testing is going on + var isTestingMic: Bool = false { + didSet { + startMicTestBtn.isHidden = isTestingMic + stopMicTestBtn.isHidden = !isTestingMic + startLoopbackTestBtn.isEnabled = !isTestingMic + } + } + + // indicate if speaker testing is going on + var isTestingSpeaker: Bool = false { + didSet { + startSpeakerTestBtn.isHidden = isTestingSpeaker + stopSpeakerTestBtn.isHidden = !isTestingSpeaker + startLoopbackTestBtn.isEnabled = !isTestingSpeaker + } + } + + // indicate if speaker testing is going on + var isTestingLoopback: Bool = false { + didSet { + startLoopbackTestBtn.isHidden = isTestingLoopback + stopLoopbackTestBtn.isHidden = !isTestingLoopback + + startMicTestBtn.isEnabled = !isTestingLoopback + startSpeakerTestBtn.isEnabled = !isTestingLoopback + } + } + + @IBOutlet weak var container: AGEVideoContainer! + var agoraKit: AgoraRtcEngineKit! + + override func viewDidLoad() { + super.viewDidLoad() + + layoutVideos() + + // set up agora instance when view loaded + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + // this is mandatory to get camera list + agoraKit.enableVideo() + + //find device in a separate thread to avoid blocking main thread + let queue = DispatchQueue(label: "device.enumerateDevices") + queue.async {[unowned self] in + self.cameras = self.agoraKit.enumerateDevices(.videoCapture) ?? [] + self.mics = self.agoraKit.enumerateDevices(.audioRecording) ?? [] + self.speakers = self.agoraKit.enumerateDevices(.audioPlayout) ?? [] + } + } + + override func viewWillBeRemovedFromSplitView() { + timer?.invalidate() + agoraKit.stopEchoTest() + agoraKit.stopLastmileProbeTest() + AgoraRtcEngineKit.destroy() + } + + @IBAction func onStartCameraTest(_ sender:NSButton) { + // use selected devices + if let cameraId = cameras[cameraPicker.indexOfSelectedItem].deviceId { + agoraKit.setDevice(.videoCapture, deviceId: cameraId) + } + agoraKit.startCaptureDeviceTest(videos[0]) + isTestingCamera = true + } + + @IBAction func onStopCameraTest(_ sender:NSButton) { + agoraKit.stopCaptureDeviceTest() + isTestingCamera = false + } + + @IBAction func onStartMicTest(_ sender:NSButton) { + // use selected devices + if let micId = mics[micPicker.indexOfSelectedItem].deviceId { + agoraKit.setDevice(.audioRecording, deviceId: micId) + } + agoraKit.startRecordingDeviceTest(50) + isTestingMic = true + } + + @IBAction func onStopMicTest(_ sender:NSButton) { + agoraKit.stopRecordingDeviceTest() + isTestingMic = false + } + + @IBAction func onStartSpeakerTest(_ sender:NSButton) { + // use selected devices + if let speakerId = speakers[speakerPicker.indexOfSelectedItem].deviceId { + agoraKit.setDevice(.audioPlayout, deviceId: speakerId) + } + + if let filepath = Bundle.main.path(forResource: "audiomixing", ofType: "mp3") { + let result = agoraKit.startPlaybackDeviceTest(filepath) + if result != 0 { + self.showAlert(title: "Error", message: "startAudioMixing call failed: \(result), please check your params") + } + isTestingSpeaker = true + } + } + + @IBAction func onStopSpeakerTest(_ sender:NSButton) { + agoraKit.stopPlaybackDeviceTest() + isTestingSpeaker = false + } + + @IBAction func onStartLoopbackTest(_ sender:NSButton) { + // use selected devices + if let micId = mics[micPicker.indexOfSelectedItem].deviceId { + agoraKit.setDevice(.audioRecording, deviceId: micId) + } + if let speakerId = speakers[speakerPicker.indexOfSelectedItem].deviceId { + agoraKit.setDevice(.audioPlayout, deviceId: speakerId) + } + agoraKit.startAudioDeviceLoopbackTest(50) + isTestingLoopback = true + } + + @IBAction func onStopLoopbackTest(_ sender:NSButton) { + agoraKit.stopAudioDeviceLoopbackTest() + isTestingLoopback = false + } + + @IBAction func onStartLastmileTest(_ sender:NSButton) { + lastmileActivityView.startAnimation(nil) + let config = AgoraLastmileProbeConfig() + // do uplink testing + config.probeUplink = true; + // do downlink testing + config.probeDownlink = true; + // expected uplink bitrate, range: [100000, 5000000] + config.expectedUplinkBitrate = 100000; + // expected downlink bitrate, range: [100000, 5000000] + config.expectedDownlinkBitrate = 100000; + agoraKit.startLastmileProbeTest(config) + } + + @IBAction func doEchoTest(sender: NSButton) { + agoraKit.startEchoTest(withInterval: 10) + showPopover(isValidate: false, seconds: 10) {[unowned self] in + self.showPopover(isValidate: true, seconds: 10) {[unowned self] in + self.agoraKit.stopEchoTest() + } + } + } + + // show popover and hide after seconds + func showPopover(isValidate:Bool, seconds:Int, callback:@escaping (() -> Void)) { + var count = seconds + var countDownLabel:NSTextField? + var popover:NSView? + if(isValidate) { + countDownLabel = echoValidateCountDownLabel + popover = echoValidatePopover + } else { + countDownLabel = echoTestCountDownLabel + popover = echoTestPopover + } + + countDownLabel?.stringValue = "\(count)" + popover?.isHidden = false + timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) {[unowned self] (timer) in + count -= 1 + countDownLabel?.stringValue = "\(count)" + + if(count == 0) { + self.timer?.invalidate() + popover?.isHidden = true + callback() + } + } + } + + func layoutVideos() { + let view = VideoView.createFromNib()! + view.placeholder.stringValue = "Camera Test Preview" + videos = [view] + // layout render view + container.layoutStream(views: videos) + } +} + +/// agora rtc engine delegate events +extension PrecallTest: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.rawValue) occur") + } + + /// Reports which users are speaking, the speakers' volumes, and whether the local user is speaking. + /// @params speakers volume info for all speakers + /// @params totalVolume Total volume after audio mixing. The value range is [0,255]. + func rtcEngine(_ engine: AgoraRtcEngineKit, reportAudioVolumeIndicationOfSpeakers speakers: [AgoraRtcAudioVolumeInfo], totalVolume: Int) { + for speaker in speakers { + if(speaker.uid == 0) { + micTestingVolumeIndicator.doubleValue = Double(speaker.volume) + } + } + } + + /// callback to get lastmile quality 2seconds after startLastmileProbeTest + func rtcEngine(_ engine: AgoraRtcEngineKit, lastmileQuality quality: AgoraNetworkQuality) { + lastmileResultLabel.stringValue = "Quality: \(quality.description())" + } + + /// callback to get more detail lastmile quality after startLastmileProbeTest + func rtcEngine(_ engine: AgoraRtcEngineKit, lastmileProbeTest result: AgoraLastmileProbeResult) { + let rtt = "Rtt: \(result.rtt)ms" + let downlinkBandwidth = "DownlinkAvailableBandwidth: \(result.downlinkReport.availableBandwidth)Kbps" + let downlinkJitter = "DownlinkJitter: \(result.downlinkReport.jitter)ms" + let downlinkLoss = "DownlinkLoss: \(result.downlinkReport.packetLossRate)%" + + let uplinkBandwidth = "UplinkAvailableBandwidth: \(result.uplinkReport.availableBandwidth)Kbps" + let uplinkJitter = "UplinkJitter: \(result.uplinkReport.jitter)ms" + let uplinkLoss = "UplinkLoss: \(result.uplinkReport.packetLossRate)%" + + lastmileProbResultLabel.stringValue = [rtt, downlinkBandwidth, downlinkJitter, downlinkLoss, uplinkBandwidth, uplinkJitter, uplinkLoss].joined(separator: "\n") + + // stop testing after get last mile detail result + engine.stopLastmileProbeTest() + lastmileActivityView.stopAnimation(nil) + } +} diff --git a/macOS/APIExample/Examples/Advanced/PrecallTest/zh-Hans.lproj/PrecallTest.strings b/macOS/APIExample/Examples/Advanced/PrecallTest/zh-Hans.lproj/PrecallTest.strings new file mode 100644 index 000000000..3207a8047 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/PrecallTest/zh-Hans.lproj/PrecallTest.strings @@ -0,0 +1,45 @@ + +/* Class = "NSButtonCell"; title = "Stop Test"; ObjectID = "4f3-Ea-NwT"; */ +"4f3-Ea-NwT.title" = "停止测试"; + +/* Class = "NSButtonCell"; title = "Start Test"; ObjectID = "4z6-Jy-1cc"; */ +"4z6-Jy-1cc.title" = "开始测试"; + +/* Class = "NSButtonCell"; title = "Start Test"; ObjectID = "5jA-zT-2bv"; */ +"5jA-zT-2bv.title" = "开始测试"; + +/* Class = "NSButtonCell"; title = "Stop Audio Device Loopback Test"; ObjectID = "BJO-I0-Opi"; */ +"BJO-I0-Opi.title" = "停止本地音频回路测试"; + +/* Class = "NSTextFieldCell"; title = "Please say something.."; ObjectID = "BPe-Gx-enC"; */ +"BPe-Gx-enC.title" = "尝试说一些话..."; + +/* Class = "NSViewController"; title = "Custom Video Source(MediaIO)"; ObjectID = "Gwp-vd-c2J"; */ +"Gwp-vd-c2J.title" = "通话前测试"; + +/* Class = "NSTextFieldCell"; title = "10"; ObjectID = "L6F-q4-SNZ"; */ +"L6F-q4-SNZ.title" = "10"; + +/* Class = "NSTextFieldCell"; title = "Now you should hear what you said..."; ObjectID = "Yjn-ei-T3i"; */ +"Yjn-ei-T3i.title" = "现在你应该能听到前10秒的声音..."; + +/* Class = "NSTextFieldCell"; title = "10"; ObjectID = "aQJ-oH-NdD"; */ +"aQJ-oH-NdD.title" = "10"; + +/* Class = "NSButtonCell"; title = "Stop Test"; ObjectID = "bGT-vl-2FZ"; */ +"bGT-vl-2FZ.title" = "停止测试"; + +/* Class = "NSButtonCell"; title = "Start Echo Test"; ObjectID = "cTC-4D-0SS"; */ +"cTC-4D-0SS.title" = "开始回声测试"; + +/* Class = "NSButtonCell"; title = "Start Audio Device Loopback Test"; ObjectID = "fhC-uz-lo8"; */ +"fhC-uz-lo8.title" = "开始本地音频回路测试"; + +/* Class = "NSButtonCell"; title = "Start Lastmile Test"; ObjectID = "flT-Cc-shZ"; */ +"flT-Cc-shZ.title" = "开始Lastmile网络测试"; + +/* Class = "NSButtonCell"; title = "Stop Test"; ObjectID = "oar-3q-rdY"; */ +"oar-3q-rdY.title" = "停止测试"; + +/* Class = "NSButtonCell"; title = "Start Test"; ObjectID = "xsZ-UP-eoO"; */ +"xsZ-UP-eoO.title" = "开始测试"; diff --git a/macOS/APIExample/Examples/Advanced/RTMPStreaming/Base.lproj/RTMPStreaming.storyboard b/macOS/APIExample/Examples/Advanced/RTMPStreaming/Base.lproj/RTMPStreaming.storyboard new file mode 100644 index 000000000..9edb8563f --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/RTMPStreaming/Base.lproj/RTMPStreaming.storyboard @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Examples/Advanced/RTMPStreaming/RTMPStreaming.swift b/macOS/APIExample/Examples/Advanced/RTMPStreaming/RTMPStreaming.swift new file mode 100644 index 000000000..296c9f26c --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/RTMPStreaming/RTMPStreaming.swift @@ -0,0 +1,352 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import Cocoa +import AgoraRtcKit +import AGEVideoLayout + +let CANVAS_WIDTH = 640 +let CANVAS_HEIGHT = 480 + +class RTMPStreaming: BaseViewController { + var videos: [VideoView] = [] + + @IBOutlet weak var Container: AGEVideoContainer! + + var agoraKit: AgoraRtcEngineKit! + + var transcoding = AgoraLiveTranscoding.default() + + /** + --- rtmpUrls Picker --- + */ + @IBOutlet weak var selectRtmpUrlsPicker: Picker! + @IBOutlet weak var removeURLBtn: NSButton! + @IBOutlet weak var removeAllURLBtn: NSButton! + var rtmpURLs: [String] = [] + var selectedrtmpUrl: String? { + let index = self.selectRtmpUrlsPicker.indexOfSelectedItem + if index >= 0 && index < rtmpURLs.count { + return rtmpURLs[index] + } else { + return nil + } + } + func initSelectRtmpUrlsPicker() { + selectRtmpUrlsPicker.label.stringValue = "urls" + selectRtmpUrlsPicker.picker.addItems(withTitles: rtmpURLs) + } + /// callback when remove streaming url button hit + @IBAction func onRemoveStreamingURL(_ sender: Any) { + guard let selectedURL = selectedrtmpUrl else { return } + agoraKit.removePublishStreamUrl(selectedURL) + rtmpURLs.remove(at: selectRtmpUrlsPicker.indexOfSelectedItem) + selectRtmpUrlsPicker.picker.removeItem(at: selectRtmpUrlsPicker.indexOfSelectedItem) + } + + /// callback when remove all streaming url button hit + @IBAction func onRemoveAllStreamingURL(_ sender: Any) { + for url in rtmpURLs { + agoraKit.removePublishStreamUrl(url) + } + rtmpURLs = [] + selectRtmpUrlsPicker.picker.removeAllItems() + } + + /** + --- Channel TextField --- + */ + @IBOutlet weak var channelField: Input! + func initChannelField() { + channelField.label.stringValue = "Channel".localized + channelField.field.placeholderString = "Channel Name".localized + } + + /** + --- rtmp TextField --- + */ + @IBOutlet weak var rtmpURLField: Input! + @IBOutlet weak var transcodingCheckBox: NSButton! + var transcodingEnabled: Bool { + get { + return transcodingCheckBox.state == .on + } + } + @IBOutlet weak var addURLBtn: NSButton! + func initRtmpURLField() { + rtmpURLField.label.stringValue = "rtmp" + rtmpURLField.field.placeholderString = "rtmp://" + } + /// callback when publish button hit + @IBAction func onAddStreamingURL(_ sender: Any) { + //let transcodingEnabled = transcodingCheckBox.state == .on + let rtmpURL = rtmpURLField.stringValue + if(rtmpURL.isEmpty || !rtmpURL.starts(with: "rtmp://")) { + showAlert(title: "Add Streaming URL Failed", message: "RTMP URL cannot be empty or not start with 'rtmp://'") + return + } + + if transcodingEnabled { + // we will use transcoding to composite multiple hosts' video + // therefore we have to create a livetranscoding object and call before addPublishStreamUrl + transcoding.size = CGSize(width: CANVAS_WIDTH, height: CANVAS_HEIGHT) + agoraKit.setLiveTranscoding(transcoding) + } + + // start publishing to this URL + agoraKit.addPublishStreamUrl(rtmpURL, transcodingEnabled: transcodingEnabled) + // update properties and UI + rtmpURLs.append(rtmpURL) + selectRtmpUrlsPicker.picker.addItem(withTitle: rtmpURL) + } + + /** + --- Button --- + */ + @IBOutlet weak var joinChannelButton: NSButton! + func initJoinChannelButton() { + joinChannelButton.title = isJoined ? "Leave Channel".localized : "Join Channel".localized + } + + // indicate if current instance has joined channel + var isJoined: Bool = false { + didSet { + channelField.isEnabled = !isJoined + initJoinChannelButton() + } + } + + // indicate for doing something + var isProcessing: Bool = false { + didSet { + joinChannelButton.isEnabled = !isProcessing + } + } + + override func viewDidLoad() { + super.viewDidLoad() + layoutVideos(2) + // Do view setup here. + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + agoraKit.enableVideo() + + initSelectRtmpUrlsPicker() + initRtmpURLField() + initChannelField() + initJoinChannelButton() + } + + override func viewWillBeRemovedFromSplitView() { + if isJoined { + agoraKit.leaveChannel { (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + } + } + } + + @IBAction func onJoinPressed(_ sender:Any) { + if !isJoined { + // check configuration + let channel = channelField.stringValue + if channel.isEmpty { + return + } + // set live broadcaster mode + agoraKit.setChannelProfile(.liveBroadcasting) + // set myself as broadcaster to stream video/audio + agoraKit.setClientRole(.broadcaster) + // enable video module and set up video encoding configs + agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: Configs.Resolutions[GlobalSettings.shared.resolutionSetting.selectedOption().value].size(), + frameRate: AgoraVideoFrameRate(rawValue: Configs.Fps[GlobalSettings.shared.fpsSetting.selectedOption().value]) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + // set up local video to render your local camera preview + let localVideo = videos[0] + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + // the view to be binded + videoCanvas.view = localVideo.videocanvas + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + isProcessing = true + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channel, info: nil, uid: 0, options: option) + if result != 0 { + isProcessing = false + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } else { + isProcessing = true + agoraKit.leaveChannel { [unowned self] (stats:AgoraChannelStats) in + self.isProcessing = false + LogUtils.log(message: "Left channel", level: .info) + self.videos[0].uid = nil + self.isJoined = false + self.videos.forEach { + $0.uid = nil + $0.statsLabel.stringValue = "" + } + } + } + } + + func layoutVideos(_ count: Int) { + videos = [] + for i in 0...count - 1 { + let view = VideoView.createFromNib()! + if(i == 0) { + view.placeholder.stringValue = "Local" + } else { + view.placeholder.stringValue = "Remote \(i)" + } + videos.append(view) + } + // layout render view + Container.layoutStream(views: videos) + } +} + +/// agora rtc engine delegate events +extension RTMPStreaming: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + if isProcessing { + isProcessing = false + } + self.showAlert(title: "Error", message: "Error \(errorCode.rawValue) occur") + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isProcessing = false + isJoined = true + let localVideo = videos[0] + localVideo.uid = uid + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + + // add transcoding user so the video stream will be involved + // in future RTMP Stream + let user = AgoraLiveTranscodingUser() + user.rect = CGRect(x: 0, y: 0, width: CANVAS_WIDTH / 2, height: CANVAS_HEIGHT) + user.uid = uid + transcoding.add(user) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // find a VideoView w/o uid assigned + if let remoteVideo = videos.first(where: { $0.uid == nil }) { + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videocanvas + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + remoteVideo.uid = uid + } else { + LogUtils.log(message: "no video canvas available for \(uid), cancel bind", level: .warning) + } + + // update live transcoding + // add new user onto the canvas + let user = AgoraLiveTranscodingUser() + user.rect = CGRect(x: CANVAS_WIDTH / 2, y: 0, width: CANVAS_WIDTH / 2, height: CANVAS_HEIGHT) + user.uid = uid + self.transcoding.add(user) + // remember you need to call setLiveTranscoding again if you changed the layout + agoraKit.setLiveTranscoding(transcoding) + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + if let remoteVideo = videos.first(where: { $0.uid == uid }) { + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + remoteVideo.uid = nil + } else { + LogUtils.log(message: "no matching video canvas for \(uid), cancel unbind", level: .warning) + } + + // remove user from canvas if current cohost left channel + transcoding.removeUser(uid) + // remember you need to call setLiveTranscoding again if you changed the layout + agoraKit.setLiveTranscoding(transcoding) + } + + /// callback for state of rtmp streaming, for both good and bad state + /// @param url rtmp streaming url + /// @param state state of rtmp streaming + /// @param reason + func rtcEngine(_ engine: AgoraRtcEngineKit, rtmpStreamingChangedToState url: String, state: AgoraRtmpStreamingState, errorCode: AgoraRtmpStreamingErrorCode) { + LogUtils.log(message: "rtmp streaming: \(url) state \(state.rawValue) error \(errorCode.rawValue)", level: .info) + if(state == .running) { + self.showAlert(title: "Notice", message: "\(url) Publish Success") + } else if(state == .failure) { + self.showAlert(title: "Error", message: "\(url) Publish Failed: \(errorCode.rawValue)") + } else if(state == .idle) { + self.showAlert(title: "Notice", message: "\(url) Publish Stopped") + } + } + + /// callback when live transcoding is properly updated + func rtcEngineTranscodingUpdated(_ engine: AgoraRtcEngineKit) { + LogUtils.log(message: "live transcoding updated", level: .info) + } +} diff --git a/macOS/APIExample/Examples/Advanced/RTMPStreaming/zh-Hans.lproj/RTMPStreaming.strings b/macOS/APIExample/Examples/Advanced/RTMPStreaming/zh-Hans.lproj/RTMPStreaming.strings new file mode 100644 index 000000000..c471fbd9e --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/RTMPStreaming/zh-Hans.lproj/RTMPStreaming.strings @@ -0,0 +1,27 @@ + +/* Class = "NSButtonCell"; title = "Join"; ObjectID = "06A-fH-QIv"; */ +"06A-fH-QIv.title" = "加入频道"; + +/* Class = "NSTextFieldCell"; placeholderString = "rtmp://"; ObjectID = "LvF-qW-J2U"; */ +"LvF-qW-J2U.placeholderString" = "rtmp://"; + +/* Class = "NSButtonCell"; title = "Add Streaming URL"; ObjectID = "LwR-8Z-de2"; */ +"LwR-8Z-de2.title" = "添加推流地址"; + +/* Class = "NSTextFieldCell"; placeholderString = "加入频道"; ObjectID = "UGj-Te-IEu"; */ +"UGj-Te-IEu.placeholderString" = "输入频道名"; + +/* Class = "NSViewController"; title = "RTMP Streaming"; ObjectID = "aK7-YG-lDw"; */ +"aK7-YG-lDw.title" = "RTMP旁路推流"; + +/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "dYR-6U-xkr"; */ +"dYR-6U-xkr.title" = "离开频道"; + +/* Class = "NSButtonCell"; title = "Remove All"; ObjectID = "oLm-T5-8kd"; */ +"oLm-T5-8kd.title" = "移除所有地址"; + +/* Class = "NSButtonCell"; title = "Remove Streaming URL"; ObjectID = "wDa-VN-Rvd"; */ +"wDa-VN-Rvd.title" = "移除推流地址"; + +/* Class = "NSButtonCell"; title = "Transcoding"; ObjectID = "yMt-d6-3US"; */ +"yMt-d6-3US.title" = "转码"; diff --git a/macOS/APIExample/Examples/Advanced/RawAudioData/RawAudioData.storyboard b/macOS/APIExample/Examples/Advanced/RawAudioData/RawAudioData.storyboard new file mode 100644 index 000000000..bb9766368 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/RawAudioData/RawAudioData.storyboard @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Examples/Advanced/RawAudioData/RawAudioData.swift b/macOS/APIExample/Examples/Advanced/RawAudioData/RawAudioData.swift new file mode 100644 index 000000000..60b42a533 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/RawAudioData/RawAudioData.swift @@ -0,0 +1,323 @@ +// +// RawAudioData.swift +// APIExample +// +// Created by XC on 2020/12/29. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import Cocoa +import AgoraRtcKit +import AGEVideoLayout + +class RawAudioData: BaseViewController { + var videos: [VideoView] = [] + + var agoraKit: AgoraRtcEngineKit! + + @IBOutlet weak var Container: AGEVideoContainer! + + /** + --- Microphones Picker --- + */ + @IBOutlet weak var selectMicsPicker: Picker! + var mics: [AgoraRtcDeviceInfo] = [] { + didSet { + DispatchQueue.main.async {[unowned self] in + self.selectMicsPicker.picker.addItems(withTitles: self.mics.map {$0.deviceName ?? "unknown"}) + } + } + } + var selectedMicrophone: AgoraRtcDeviceInfo? { + let index = self.selectMicsPicker.indexOfSelectedItem + if index >= 0 && index < mics.count { + return mics[index] + } else { + return nil + } + } + func initSelectMicsPicker() { + selectMicsPicker.label.stringValue = "Microphone".localized + // find device in a separate thread to avoid blocking main thread + let queue = DispatchQueue(label: "device.enumerateDevices") + queue.async {[unowned self] in + self.mics = self.agoraKit.enumerateDevices(.audioRecording) ?? [] + } + + selectMicsPicker.onSelectChanged { + if !self.isJoined { + return + } + // use selected devices + guard let micId = self.selectedMicrophone?.deviceId else { + return + } + self.agoraKit.setDevice(.audioRecording, deviceId: micId) + } + } + + /** + --- Layout Picker --- + */ + @IBOutlet weak var selectLayoutPicker: Picker! + let layouts = [Layout("1v1", 2), Layout("1v3", 4), Layout("1v8", 9), Layout("1v15", 16)] + var selectedLayout: Layout? { + let index = self.selectLayoutPicker.indexOfSelectedItem + if index >= 0 && index < layouts.count { + return layouts[index] + } else { + return nil + } + } + func initSelectLayoutPicker() { + layoutVideos(2) + selectLayoutPicker.label.stringValue = "Layout".localized + selectLayoutPicker.picker.addItems(withTitles: layouts.map { $0.label }) + selectLayoutPicker.onSelectChanged { + if self.isJoined { + return + } + guard let layout = self.selectedLayout else { return } + self.layoutVideos(layout.value) + } + } + + /** + --- Channel TextField --- + */ + @IBOutlet weak var channelField: Input! + func initChannelField() { + channelField.label.stringValue = "Channel".localized + channelField.field.placeholderString = "Channel Name".localized + } + + /** + --- Button --- + */ + @IBOutlet weak var joinChannelButton: NSButton! + func initJoinChannelButton() { + joinChannelButton.title = isJoined ? "Leave Channel".localized : "Join Channel".localized + } + + // indicate if current instance has joined channel + var isJoined: Bool = false { + didSet { + channelField.isEnabled = !isJoined + selectLayoutPicker.isEnabled = !isJoined + initJoinChannelButton() + } + } + + // indicate for doing something + var isProcessing: Bool = false { + didSet { + joinChannelButton.isEnabled = !isProcessing + } + } + + override func viewDidLoad() { + super.viewDidLoad() + // Do view setup here. + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + agoraKit.enableVideo() + + initSelectMicsPicker() + initSelectLayoutPicker() + initChannelField() + initJoinChannelButton() + } + + override func viewWillBeRemovedFromSplitView() { + if isJoined { + agoraKit.leaveChannel { (stats:AgoraChannelStats) in + // unregister AudioFrameDelegate + self.agoraKit.setAudioFrameDelegate(nil) + LogUtils.log(message: "Left channel", level: .info) + } + } + } + + @IBAction func onJoinPressed(_ sender:Any) { + if !isJoined { + // check configuration + let channel = channelField.stringValue + if channel.isEmpty { + return + } + guard let micId = selectedMicrophone?.deviceId else { + return + } + agoraKit.setDevice(.audioRecording, deviceId: micId) + // disable video module in audio scene + agoraKit.disableVideo() + // set live broadcaster mode + agoraKit.setChannelProfile(.liveBroadcasting) + // set myself as broadcaster to stream audio + agoraKit.setClientRole(.broadcaster) + + agoraKit.setRecordingAudioFrameParametersWithSampleRate(44100, channel: 1, mode: .readWrite, samplesPerCall: 4410) + agoraKit.setMixedAudioFrameParametersWithSampleRate(44100, samplesPerCall: 4410) + agoraKit.setPlaybackAudioFrameParametersWithSampleRate(44100, channel: 1, mode: .readWrite, samplesPerCall: 4410) + // Register audio observer + agoraKit.setAudioFrameDelegate(self) + // set up local video to render your local camera preview + let localVideo = videos[0] + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + // the view to be binded + videoCanvas.view = localVideo.videocanvas + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + isProcessing = true + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channel, info: nil, uid: 0, options: option) + if result != 0 { + isProcessing = false + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } else { + isProcessing = true + agoraKit.leaveChannel { [unowned self] (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + self.isProcessing = false + self.videos[0].uid = nil + self.isJoined = false + self.videos.forEach { + $0.uid = nil + $0.statsLabel.stringValue = "" + } + } + } + } + + func layoutVideos(_ count: Int) { + videos = [] + for i in 0...count - 1 { + let view = VideoView.createFromNib()! + if(i == 0) { + view.placeholder.stringValue = "Local" + } else { + view.placeholder.stringValue = "Remote \(i)" + } + videos.append(view) + } + // layout render view + Container.layoutStream(views: videos) + } +} + +/// agora rtc engine delegate events +extension RawAudioData: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + if isProcessing { + isProcessing = false + } + self.showAlert(title: "Error", message: "Error \(errorCode.rawValue) occur") + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isProcessing = false + isJoined = true + let localVideo = videos[0] + localVideo.uid = uid + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // find a VideoView w/o uid assigned + if let remoteVideo = videos.first(where: { $0.uid == nil }) { + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videocanvas + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + remoteVideo.uid = uid + } else { + LogUtils.log(message: "no video canvas available for \(uid), cancel bind", level: .warning) + } + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + if let remoteVideo = videos.first(where: { $0.uid == uid }) { + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + remoteVideo.uid = nil + } else { + LogUtils.log(message: "no matching video canvas for \(uid), cancel unbind", level: .warning) + } + } +} + +// audio data plugin, here you can process raw audio data +// note this all happens in CPU so it comes with a performance cost +extension RawAudioData: AgoraAudioFrameDelegate { + func onRecord(_ frame: AgoraAudioFrame) -> Bool { + return true + } + + func onPlaybackAudioFrame(_ frame: AgoraAudioFrame) -> Bool { + return true + } + + func onMixedAudioFrame(_ frame: AgoraAudioFrame) -> Bool { + return true + } + + func onPlaybackAudioFrame(beforeMixing frame: AgoraAudioFrame, uid: UInt) -> Bool { + return true + } +} diff --git a/macOS/APIExample/Examples/Advanced/RawMediaData/Base.lproj/RawMediaData.storyboard b/macOS/APIExample/Examples/Advanced/RawMediaData/Base.lproj/RawMediaData.storyboard new file mode 100644 index 000000000..3c89dbc14 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/RawMediaData/Base.lproj/RawMediaData.storyboard @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Examples/Advanced/RawMediaData/RawMediaData.swift b/macOS/APIExample/Examples/Advanced/RawMediaData/RawMediaData.swift new file mode 100644 index 000000000..518ad32ba --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/RawMediaData/RawMediaData.swift @@ -0,0 +1,455 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import Cocoa +import AgoraRtcKit +import AGEVideoLayout + +class RawMediaData: BaseViewController { + var videos: [VideoView] = [] + + var agoraKit: AgoraRtcEngineKit! + + var agoraMediaDataPlugin: AgoraMediaDataPlugin? + + @IBOutlet weak var Container: AGEVideoContainer! + + /** + --- Resolutions Picker --- + */ + @IBOutlet weak var selectResolutionPicker: Picker! + var selectedResolution: Resolution? { + let index = self.selectResolutionPicker.indexOfSelectedItem + if index >= 0 && index < Configs.Resolutions.count { + return Configs.Resolutions[index] + } else { + return nil + } + } + func initSelectResolutionPicker() { + selectResolutionPicker.label.stringValue = "Resolution".localized + selectResolutionPicker.picker.addItems(withTitles: Configs.Resolutions.map { $0.name() }) + selectResolutionPicker.picker.selectItem(at: GlobalSettings.shared.resolutionSetting.selectedOption().value) + + selectResolutionPicker.onSelectChanged { + if !self.isJoined { + return + } + + guard let resolution = self.selectedResolution, + let fps = self.selectedFps else { + return + } + self.agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + } + } + + /** + --- Fps Picker --- + */ + @IBOutlet weak var selectFpsPicker: Picker! + var selectedFps: Int? { + let index = self.selectFpsPicker.indexOfSelectedItem + if index >= 0 && index < Configs.Fps.count { + return Configs.Fps[index] + } else { + return nil + } + } + func initSelectFpsPicker() { + selectFpsPicker.label.stringValue = "Frame Rate".localized + selectFpsPicker.picker.addItems(withTitles: Configs.Fps.map { "\($0)fps" }) + selectFpsPicker.picker.selectItem(at: GlobalSettings.shared.fpsSetting.selectedOption().value) + + selectFpsPicker.onSelectChanged { + if !self.isJoined { + return + } + guard let resolution = self.selectedResolution, + let fps = self.selectedFps else { + return + } + self.agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + } + } + + /** + --- Layout Picker --- + */ + @IBOutlet weak var selectLayoutPicker: Picker! + let layouts = [Layout("1v1", 2), Layout("1v3", 4), Layout("1v8", 9), Layout("1v15", 16)] + var selectedLayout: Layout? { + let index = self.selectLayoutPicker.indexOfSelectedItem + if index >= 0 && index < layouts.count { + return layouts[index] + } else { + return nil + } + } + func initSelectLayoutPicker() { + layoutVideos(2) + selectLayoutPicker.label.stringValue = "Layout".localized + selectLayoutPicker.picker.addItems(withTitles: layouts.map { $0.label }) + selectLayoutPicker.onSelectChanged { + if self.isJoined { + return + } + guard let layout = self.selectedLayout else { return } + self.layoutVideos(layout.value) + } + } + + /** + --- Channel TextField --- + */ + @IBOutlet weak var channelField: Input! + func initChannelField() { + channelField.label.stringValue = "Channel".localized + channelField.field.placeholderString = "Channel Name".localized + } + + /** + --- Button --- + */ + @IBOutlet weak var joinChannelButton: NSButton! + func initJoinChannelButton() { + joinChannelButton.title = isJoined ? "Leave Channel".localized : "Join Channel".localized + } + + // indicate if current instance has joined channel + var isJoined: Bool = false { + didSet { + channelField.isEnabled = !isJoined + selectLayoutPicker.isEnabled = !isJoined + initJoinChannelButton() + } + } + + // indicate for doing something + var isProcessing: Bool = false { + didSet { + joinChannelButton.isEnabled = !isProcessing + } + } + + override func viewDidLoad() { + super.viewDidLoad() + // Do view setup here. + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + agoraKit.enableVideo() + + initSelectResolutionPicker() + initSelectFpsPicker() + initSelectLayoutPicker() + initChannelField() + initJoinChannelButton() + } + + override func viewWillBeRemovedFromSplitView() { + if isJoined { + // deregister video observer + let videoType:ObserverVideoType = ObserverVideoType(rawValue: ObserverVideoType.captureVideo.rawValue | ObserverVideoType.renderVideo.rawValue | ObserverVideoType.preEncodeVideo.rawValue) + agoraMediaDataPlugin?.deregisterVideoRawDataObserver(videoType) + + // deregister audio observer + let audioType:ObserverAudioType = ObserverAudioType(rawValue: ObserverAudioType.recordAudio.rawValue | ObserverAudioType.playbackAudioFrameBeforeMixing.rawValue | ObserverAudioType.mixedAudio.rawValue | ObserverAudioType.playbackAudio.rawValue) ; + agoraMediaDataPlugin?.deregisterAudioRawDataObserver(audioType) + + // deregister packet observer + let packetType:ObserverPacketType = ObserverPacketType(rawValue: ObserverPacketType.sendAudio.rawValue | ObserverPacketType.sendVideo.rawValue | ObserverPacketType.receiveAudio.rawValue | ObserverPacketType.receiveVideo.rawValue) + agoraMediaDataPlugin?.deregisterPacketRawDataObserver(packetType) + + agoraKit.leaveChannel { (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + } + } + } + + func registerAgoraMediaDataPlugin() { + if agoraMediaDataPlugin == nil { + // setup raw media data observers + agoraMediaDataPlugin = AgoraMediaDataPlugin(agoraKit: agoraKit) + + // Register audio observer + let audioType: ObserverAudioType = ObserverAudioType(rawValue: ObserverAudioType.recordAudio.rawValue | ObserverAudioType.playbackAudioFrameBeforeMixing.rawValue | ObserverAudioType.mixedAudio.rawValue | ObserverAudioType.playbackAudio.rawValue) ; + agoraMediaDataPlugin?.registerAudioRawDataObserver(audioType) + agoraMediaDataPlugin?.audioDelegate = self + + // Register video observer + let videoType: ObserverVideoType = ObserverVideoType(rawValue: ObserverVideoType.captureVideo.rawValue | ObserverVideoType.renderVideo.rawValue | ObserverVideoType.preEncodeVideo.rawValue) + agoraMediaDataPlugin?.registerVideoRawDataObserver(videoType) + agoraMediaDataPlugin?.videoDelegate = self; + + // Register packet observer + let packetType: ObserverPacketType = ObserverPacketType(rawValue: ObserverPacketType.sendAudio.rawValue | ObserverPacketType.sendVideo.rawValue | ObserverPacketType.receiveAudio.rawValue | ObserverPacketType.receiveVideo.rawValue) + agoraMediaDataPlugin?.registerPacketRawDataObserver(packetType) + agoraMediaDataPlugin?.packetDelegate = self; + } + + agoraKit.setRecordingAudioFrameParametersWithSampleRate(44100, channel: 1, mode: .readWrite, samplesPerCall: 4410) + agoraKit.setMixedAudioFrameParametersWithSampleRate(44100, samplesPerCall: 4410) + agoraKit.setPlaybackAudioFrameParametersWithSampleRate(44100, channel: 1, mode: .readWrite, samplesPerCall: 4410) + } + + @IBAction func onJoinPressed(_ sender:Any) { + if !isJoined { + // check configuration + let channel = channelField.stringValue + if channel.isEmpty { + return + } + guard let resolution = selectedResolution, + let fps = selectedFps else { + return + } + // set live broadcaster mode + agoraKit.setChannelProfile(.liveBroadcasting) + // set myself as broadcaster to stream video/audio + agoraKit.setClientRole(.broadcaster) + // enable video module and set up video encoding configs + agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + registerAgoraMediaDataPlugin() + + // set up local video to render your local camera preview + let localVideo = videos[0] + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + // the view to be binded + videoCanvas.view = localVideo.videocanvas + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + isProcessing = true + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channel, info: nil, uid: 0, options: option) + if result != 0 { + isProcessing = false + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } else { + isProcessing = true + agoraKit.leaveChannel { [unowned self] (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + self.isProcessing = false + self.videos[0].uid = nil + self.isJoined = false + self.videos.forEach { + $0.uid = nil + $0.statsLabel.stringValue = "" + } + } + } + } + + func layoutVideos(_ count: Int) { + videos = [] + for i in 0...count - 1 { + let view = VideoView.createFromNib()! + if(i == 0) { + view.placeholder.stringValue = "Local" + } else { + view.placeholder.stringValue = "Remote \(i)" + } + videos.append(view) + } + // layout render view + Container.layoutStream(views: videos) + } +} + +/// agora rtc engine delegate events +extension RawMediaData: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + if isProcessing { + isProcessing = false + } + self.showAlert(title: "Error", message: "Error \(errorCode.rawValue) occur") + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isProcessing = false + isJoined = true + let localVideo = videos[0] + localVideo.uid = uid + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // find a VideoView w/o uid assigned + if let remoteVideo = videos.first(where: { $0.uid == nil }) { + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videocanvas + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + remoteVideo.uid = uid + } else { + LogUtils.log(message: "no video canvas available for \(uid), cancel bind", level: .warning) + } + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + if let remoteVideo = videos.first(where: { $0.uid == uid }) { + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + remoteVideo.uid = nil + } else { + LogUtils.log(message: "no matching video canvas for \(uid), cancel unbind", level: .warning) + } + } +} + + +// audio data plugin, here you can process raw audio data +// note this all happens in CPU so it comes with a performance cost +extension RawMediaData: AgoraAudioDataPluginDelegate { + /// Retrieves the recorded audio frame. + func mediaDataPlugin(_ mediaDataPlugin: AgoraMediaDataPlugin, didRecord audioRawData: AgoraAudioRawData) -> AgoraAudioRawData { + return audioRawData + } + + /// Retrieves the audio playback frame for getting the audio. + func mediaDataPlugin(_ mediaDataPlugin: AgoraMediaDataPlugin, willPlaybackAudioRawData audioRawData: AgoraAudioRawData) -> AgoraAudioRawData { + return audioRawData + } + + /// Retrieves the audio frame of a specified user before mixing. + /// The SDK triggers this callback if isMultipleChannelFrameWanted returns false. + func mediaDataPlugin(_ mediaDataPlugin: AgoraMediaDataPlugin, willPlaybackBeforeMixing audioRawData: AgoraAudioRawData, ofUid uid: uint) -> AgoraAudioRawData { + return audioRawData + } + + /// Retrieves the mixed recorded and playback audio frame. + func mediaDataPlugin(_ mediaDataPlugin: AgoraMediaDataPlugin, didMixedAudioRawData audioRawData: AgoraAudioRawData) -> AgoraAudioRawData { + return audioRawData + } +} + +// video data plugin, here you can process raw video data +// note this all happens in CPU so it comes with a performance cost +extension RawMediaData : AgoraVideoDataPluginDelegate +{ + /// Occurs each time the SDK receives a video frame captured by the local camera. + /// After you successfully register the video frame observer, the SDK triggers this callback each time a video frame is received. In this callback, you can get the video data captured by the local camera. You can then pre-process the data according to your scenarios. + /// After pre-processing, you can send the processed video data back to the SDK by setting the videoFrame parameter in this callback. + func mediaDataPlugin(_ mediaDataPlugin: AgoraMediaDataPlugin, didCapturedVideoRawData videoRawData: AgoraVideoRawData) -> AgoraVideoRawData { + return videoRawData + } + + /// Occurs each time the SDK receives a video frame before sending to encoder + /// After you successfully register the video frame observer, the SDK triggers this callback each time a video frame is going to be sent to encoder. In this callback, you can get the video data before it is sent to enoder. You can then pre-process the data according to your scenarios. + /// After pre-processing, you can send the processed video data back to the SDK by setting the videoFrame parameter in this callback. + func mediaDataPlugin(_ mediaDataPlugin: AgoraMediaDataPlugin, willPreEncode videoRawData: AgoraVideoRawData) -> AgoraVideoRawData { + return videoRawData + } + + /// Occurs each time the SDK receives a video frame sent by the remote user. + ///After you successfully register the video frame observer and isMultipleChannelFrameWanted return false, the SDK triggers this callback each time a video frame is received. In this callback, you can get the video data sent by the remote user. You can then post-process the data according to your scenarios. + ///After post-processing, you can send the processed data back to the SDK by setting the videoFrame parameter in this callback. + func mediaDataPlugin(_ mediaDataPlugin: AgoraMediaDataPlugin, willRenderVideoRawData videoRawData: AgoraVideoRawData, ofUid uid: uint) -> AgoraVideoRawData { + return videoRawData + } +} + +// packet data plugin, here you can process raw network packet(before decoding/encoding) +// note this all happens in CPU so it comes with a performance cost +extension RawMediaData : AgoraPacketDataPluginDelegate +{ + /// Occurs when the local user sends a video packet. + func mediaDataPlugin(_ mediaDataPlugin: AgoraMediaDataPlugin, willSendVideoPacket videoPacket: AgoraPacketRawData) -> AgoraPacketRawData { + return videoPacket + } + + /// Occurs when the local user sends an audio packet. + func mediaDataPlugin(_ mediaDataPlugin: AgoraMediaDataPlugin, willSendAudioPacket audioPacket: AgoraPacketRawData) -> AgoraPacketRawData { + return audioPacket + } + + /// Occurs when the local user receives a video packet. + func mediaDataPlugin(_ mediaDataPlugin: AgoraMediaDataPlugin, didReceivedVideoPacket videoPacket: AgoraPacketRawData) -> AgoraPacketRawData { + return videoPacket + } + + /// Occurs when the local user receives an audio packet. + func mediaDataPlugin(_ mediaDataPlugin: AgoraMediaDataPlugin, didReceivedAudioPacket audioPacket: AgoraPacketRawData) -> AgoraPacketRawData { + return audioPacket + } +} diff --git a/macOS/APIExample/Examples/Advanced/RawMediaData/zh-Hans.lproj/RawMediaData.strings b/macOS/APIExample/Examples/Advanced/RawMediaData/zh-Hans.lproj/RawMediaData.strings new file mode 100644 index 000000000..593d38ee3 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/RawMediaData/zh-Hans.lproj/RawMediaData.strings @@ -0,0 +1,24 @@ + +/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "3IY-9u-JQg"; */ +"3IY-9u-JQg.title" = "1V8"; + +/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "3Sc-aR-cWj"; */ +"3Sc-aR-cWj.title" = "1V1"; + +/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "5dc-P2-Umu"; */ +"5dc-P2-Umu.title" = "离开频道"; + +/* Class = "NSViewController"; title = "Raw Media Data"; ObjectID = "Lxa-cX-S9B"; */ +"Lxa-cX-S9B.title" = "音视频裸数据"; + +/* Class = "NSButtonCell"; title = "Join"; ObjectID = "jlm-ef-BJp"; */ +"jlm-ef-BJp.title" = "加入频道"; + +/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "rHp-eQ-WQs"; */ +"rHp-eQ-WQs.title" = "1V15"; + +/* Class = "NSMenuItem"; title = "1V3"; ObjectID = "rqc-6d-D6f"; */ +"rqc-6d-D6f.title" = "1V3"; + +/* Class = "NSTextFieldCell"; placeholderString = "加入频道"; ObjectID = "skD-SR-OhN"; */ +"skD-SR-OhN.placeholderString" = "输入频道名"; diff --git a/macOS/APIExample/Examples/Advanced/ScreenShare/Base.lproj/ScreenShare.storyboard b/macOS/APIExample/Examples/Advanced/ScreenShare/Base.lproj/ScreenShare.storyboard new file mode 100644 index 000000000..643474986 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/ScreenShare/Base.lproj/ScreenShare.storyboard @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Examples/Advanced/ScreenShare/ScreenShare.swift b/macOS/APIExample/Examples/Advanced/ScreenShare/ScreenShare.swift new file mode 100644 index 000000000..ca9f6d81d --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/ScreenShare/ScreenShare.swift @@ -0,0 +1,523 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import Cocoa +import AgoraRtcKit +import AGEVideoLayout + +class ScreenShare: BaseViewController { + var videos: [VideoView] = [] + + @IBOutlet weak var container: AGEVideoContainer! + + var agoraKit: AgoraRtcEngineKit! + + /** + --- Layout Picker --- + */ + @IBOutlet weak var selectLayoutPicker: Picker! + let layouts = [Layout("1v1", 2), Layout("1v3", 4), Layout("1v8", 9), Layout("1v15", 16)] + var selectedLayout: Layout? { + let index = self.selectLayoutPicker.indexOfSelectedItem + if index >= 0 && index < layouts.count { + return layouts[index] + } else { + return nil + } + } + func initSelectLayoutPicker() { + layoutVideos(2) + selectLayoutPicker.label.stringValue = "Layout".localized + selectLayoutPicker.picker.addItems(withTitles: layouts.map { $0.label }) + selectLayoutPicker.onSelectChanged { + if self.isJoined { + return + } + guard let layout = self.selectedLayout else { return } + self.layoutVideos(layout.value) + } + } + + /** + --- Resolutions Picker --- + */ + @IBOutlet weak var selectResolutionPicker: Picker! + var selectedResolution: Resolution? { + let index = self.selectResolutionPicker.indexOfSelectedItem + if index >= 0 && index < Configs.Resolutions.count { + return Configs.Resolutions[index] + } else { + return nil + } + } + func initSelectResolutionPicker() { + selectResolutionPicker.label.stringValue = "Resolution".localized + selectResolutionPicker.picker.addItems(withTitles: Configs.Resolutions.map { $0.name() }) + selectResolutionPicker.picker.selectItem(at: GlobalSettings.shared.resolutionSetting.selectedOption().value) + + selectResolutionPicker.onSelectChanged { + if !self.isJoined { + return + } + guard let resolution = self.selectedResolution, + let fps = self.selectedFps else { + return + } + if self.isScreenSharing || self.isWindowSharing { + let params = AgoraScreenCaptureParameters() + params.frameRate = fps + params.dimensions = resolution.size() + self.agoraKit.update(params) + } else { + self.agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + } + } + } + + /** + --- Fps Picker --- + */ + @IBOutlet weak var selectFpsPicker: Picker! + var selectedFps: Int? { + let index = self.selectFpsPicker.indexOfSelectedItem + if index >= 0 && index < Configs.Fps.count { + return Configs.Fps[index] + } else { + return nil + } + } + func initSelectFpsPicker() { + selectFpsPicker.label.stringValue = "Frame Rate".localized + selectFpsPicker.picker.addItems(withTitles: Configs.Fps.map { "\($0)fps" }) + selectFpsPicker.picker.selectItem(at: GlobalSettings.shared.fpsSetting.selectedOption().value) + + selectFpsPicker.onSelectChanged { + if !self.isJoined { + return + } + guard let resolution = self.selectedResolution, + let fps = self.selectedFps else { + return + } + if self.isScreenSharing || self.isWindowSharing { + let params = AgoraScreenCaptureParameters() + params.frameRate = fps + params.dimensions = resolution.size() + self.agoraKit.update(params) + } else { + self.agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + } + } + } + + /** + --- DisplayHint Picker --- + */ + @IBOutlet weak var selectDisplayHintPicker: Picker! + var displayHints = ["Default", "Motion", "Detail"] + var selectedDisplayHint: AgoraVideoContentHint? { + let index = self.selectDisplayHintPicker.indexOfSelectedItem + if index >= 0 && index < displayHints.count { + return Configs.VideoContentHints[index] + } else { + return nil + } + } + func initSelectDisplayHintPicker() { + selectDisplayHintPicker.label.stringValue = "Display Hint".localized + selectDisplayHintPicker.picker.addItems(withTitles: displayHints) + + selectDisplayHintPicker.onSelectChanged { + if !self.isJoined { + return + } + guard let displayHint = self.selectedDisplayHint else { return } + print("setScreenCapture") + self.agoraKit.setScreenCapture(displayHint) + } + } + + var windowManager: WindowList = WindowList() + var windowlist:[Window] = [], screenlist:[Window] = [] + /** + --- Screen Picker --- + */ + @IBOutlet weak var selectScreenPicker: Picker! + var selectedScreen: Window? { + let index = self.selectScreenPicker.indexOfSelectedItem + if index >= 0 && index < screenlist.count { + return screenlist[index] + } else { + return nil + } + } + func initSelectScreenPicker() { + screenlist = windowManager.items.filter({$0.type == .screen}) + selectScreenPicker.label.stringValue = "Screen Share".localized + selectScreenPicker.picker.addItems(withTitles: screenlist.map {"\($0.name ?? "Unknown")(\($0.id))"}) + } + var isScreenSharing: Bool = false { + didSet { + windowShareButton.isEnabled = !isScreenSharing + initScreenShareButton() + halfScreenShareButton.isEnabled = isScreenSharing + } + } + /** + --- Screen Share Button --- + */ + @IBOutlet weak var screenShareButton: NSButton! + func initScreenShareButton() { + screenShareButton.isEnabled = isJoined + screenShareButton.title = isScreenSharing ? "Stop Share".localized : "Display Share".localized + } + @IBAction func onScreenShare(_ sender: NSButton) { + if !isScreenSharing { + guard let resolution = self.selectedResolution, + let fps = self.selectedFps, + let screen = selectedScreen else { + return + } + let params = AgoraScreenCaptureParameters() + params.frameRate = fps + params.dimensions = resolution.size() + let result = agoraKit.startScreenCapture(byDisplayId: UInt(screen.id), rectangle: .zero, parameters: params) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "startScreenCapture call failed: \(result), please check your params") + } else { + isScreenSharing = true + } + } else { + agoraKit.stopScreenCapture() + isScreenSharing = false + } + } + + /** + --- Window Picker --- + */ + @IBOutlet weak var selectWindowPicker: Picker! + var selectedWindow: Window? { + let index = self.selectWindowPicker.indexOfSelectedItem + if index >= 0 && index < windowlist.count { + return windowlist[index] + } else { + return nil + } + } + func initSelectWindowPicker() { + windowlist = windowManager.items.filter({$0.type == .window}) + selectWindowPicker.label.stringValue = "Window Share".localized + selectWindowPicker.picker.addItems(withTitles: windowlist.map {"\($0.name ?? "Unknown")(\($0.id))"}) + } + var isWindowSharing: Bool = false { + didSet { + screenShareButton.isEnabled = !isWindowSharing + initWindowShareButton() + halfScreenShareButton.isEnabled = isWindowSharing + } + } + /** + --- Window Share Button --- + */ + @IBOutlet weak var windowShareButton: NSButton! + func initWindowShareButton() { + windowShareButton.isEnabled = isJoined + windowShareButton.title = isWindowSharing ? "Stop Share".localized : "Window Share".localized + } + @IBAction func onWindowShare(_ sender: NSButton) { + if !isWindowSharing { + guard let resolution = self.selectedResolution, + let fps = self.selectedFps, + let window = selectedWindow else { + return + } + let params = AgoraScreenCaptureParameters() + params.frameRate = fps + params.dimensions = resolution.size() + let result = agoraKit.startScreenCapture(byWindowId: UInt(window.id), rectangle: .zero, parameters: params) + if result != 0 { + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "startScreenCapture call failed: \(result), please check your params") + } else { + isWindowSharing = true + } + } else { + agoraKit.stopScreenCapture() + isWindowSharing = false + } + } + + /** + --- Half Screen Share Button --- + */ + @IBOutlet weak var halfScreenShareButton: NSButton! + func initHalfScreenShareButton() { + halfScreenShareButton.isEnabled = isJoined + halfScreenShareButton.title = "Share Half Screen".localized + } + var half = false + @IBAction func onStartShareHalfScreen(_ sender: Any) { + let rect = NSScreen.main?.frame + let region = NSMakeRect(0, 0, !half ? rect!.width/2 : rect!.width, !half ? rect!.height/2 : rect!.height) + agoraKit.updateScreenCaptureRegion(region) + half = !half + } + + /** + --- Channel TextField --- + */ + @IBOutlet weak var channelField: Input! + func initChannelField() { + channelField.label.stringValue = "Channel".localized + channelField.field.placeholderString = "Channel Name".localized + } + + /** + --- Join Button --- + */ + @IBOutlet weak var joinChannelButton: NSButton! + func initJoinChannelButton() { + joinChannelButton.title = isJoined ? "Leave Channel".localized : "Join Channel".localized + } + + // indicate if current instance has joined channel + var isJoined: Bool = false { + didSet { + channelField.isEnabled = !isJoined + selectLayoutPicker.isEnabled = !isJoined + initJoinChannelButton() + screenShareButton.isEnabled = isJoined + windowShareButton.isEnabled = isJoined + halfScreenShareButton.isEnabled = isJoined + } + } + + // indicate for doing something + var isProcessing: Bool = false { + didSet { + joinChannelButton.isEnabled = !isProcessing + } + } + + override func viewDidLoad() { + super.viewDidLoad() + // prepare window manager and list + windowManager.getList() + // Do view setup here. + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + agoraKit.enableVideo() + + initSelectResolutionPicker() + initSelectFpsPicker() + initSelectDisplayHintPicker() + initSelectLayoutPicker() + initSelectScreenPicker() + initScreenShareButton() + initSelectWindowPicker() + initWindowShareButton() + initHalfScreenShareButton() + initChannelField() + initJoinChannelButton() + } + + override func viewWillBeRemovedFromSplitView() { + if isJoined { + agoraKit.leaveChannel { (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + } + } + } + + @IBAction func onJoinPressed(_ sender:Any) { + if !isJoined { + // check configuration + let channel = channelField.stringValue + if channel.isEmpty { + return + } + guard let resolution = selectedResolution, + let fps = selectedFps else { + return + } + + // set live broadcaster mode + agoraKit.setChannelProfile(.liveBroadcasting) + // set myself as broadcaster to stream video/audio + agoraKit.setClientRole(.broadcaster) + // enable video module and set up video encoding configs + agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + // set up local video to render your local camera preview + let localVideo = videos[0] + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + // the view to be binded + videoCanvas.view = localVideo.videocanvas + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + isProcessing = true + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channel, info: nil, uid: 0, options: option) + if result != 0 { + isProcessing = false + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } else { + isProcessing = true + agoraKit.leaveChannel { [unowned self] (stats:AgoraChannelStats) in + self.isProcessing = false + LogUtils.log(message: "Left channel", level: .info) + self.videos[0].uid = nil + self.isJoined = false + self.videos.forEach { + $0.uid = nil + $0.statsLabel.stringValue = "" + } + } + } + } + + func layoutVideos(_ count: Int) { + videos = [] + for i in 0...count - 1 { + let view = VideoView.createFromNib()! + if(i == 0) { + view.placeholder.stringValue = "Local" + } else { + view.placeholder.stringValue = "Remote \(i)" + } + videos.append(view) + } + // layout render view + container.layoutStream(views: videos) + } +} + +/// agora rtc engine delegate events +extension ScreenShare: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + if isProcessing { + isProcessing = false + } + self.showAlert(title: "Error", message: "Error \(errorCode.rawValue) occur") + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isProcessing = false + isJoined = true + let localVideo = videos[0] + localVideo.uid = uid + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // find a VideoView w/o uid assigned + if let remoteVideo = videos.first(where: { $0.uid == nil }) { + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videocanvas + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + remoteVideo.uid = uid + } else { + LogUtils.log(message: "no video canvas available for \(uid), cancel bind", level: .warning) + } + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + if let remoteVideo = videos.first(where: { $0.uid == uid }) { + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + remoteVideo.uid = nil + } else { + LogUtils.log(message: "no matching video canvas for \(uid), cancel unbind", level: .warning) + } + } +} diff --git a/macOS/APIExample/Examples/Advanced/ScreenShare/zh-Hans.lproj/ScreenShare.strings b/macOS/APIExample/Examples/Advanced/ScreenShare/zh-Hans.lproj/ScreenShare.strings new file mode 100644 index 000000000..5fc55e678 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/ScreenShare/zh-Hans.lproj/ScreenShare.strings @@ -0,0 +1,42 @@ + +/* Class = "NSButtonCell"; title = "Join"; ObjectID = "1ik-om-mWj"; */ +"1ik-om-mWj.title" = "加入频道"; + +/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "6f9-0B-egB"; */ +"6f9-0B-egB.title" = "1V1"; + +/* Class = "NSButtonCell"; title = "Display Share"; ObjectID = "ACV-0l-kRZ"; */ +"ACV-0l-kRZ.title" = "屏幕共享"; + +/* Class = "NSViewController"; title = "Stream Encryption"; ObjectID = "Gwp-vd-c2J"; */ +"Gwp-vd-c2J.title" = "码流加密"; + +/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "Owt-vb-7U9"; */ +"Owt-vb-7U9.title" = "离开频道"; + +/* Class = "NSMenuItem"; title = "1V3"; ObjectID = "S4i-eh-YzK"; */ +"S4i-eh-YzK.title" = "1V3"; + +/* Class = "NSButtonCell"; title = "Stop Share"; ObjectID = "TlR-ef-9cf"; */ +"TlR-ef-9cf.title" = "停止共享"; + +/* Class = "NSTextFieldCell"; placeholderString = "加入频道"; ObjectID = "aj5-Fn-je9"; */ +"aj5-Fn-je9.placeholderString" = "输入频道名"; + +/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "cxo-X2-S8L"; */ +"cxo-X2-S8L.title" = "1V15"; + +/* Class = "NSButtonCell"; title = "Window Share"; ObjectID = "ftv-L5-p8U"; */ +"ftv-L5-p8U.title" = "窗口共享"; + +/* Class = "NSButtonCell"; title = "Stop Share"; ObjectID = "ka7-2T-SiW"; */ +"ka7-2T-SiW.title" = "停止共享"; + +/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "zu1-vg-leG"; */ +"zu1-vg-leG.title" = "1V8"; + +/* Class = "NSButtonCell"; title = "Share Half Screen"; ObjectID = "0Ao-Fe-BEt"; */ +"0Ao-Fe-BEt.title" = "分享部分区域"; + +/* Class = "NSButtonCell"; title = "Update Config"; ObjectID = "siB-l9-qc1"; */ +"siB-l9-qc1.title" = "更新参数"; diff --git a/macOS/APIExample/Examples/Advanced/StreamEncryption/Base.lproj/StreamEncryption.storyboard b/macOS/APIExample/Examples/Advanced/StreamEncryption/Base.lproj/StreamEncryption.storyboard new file mode 100644 index 000000000..3284673ef --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/StreamEncryption/Base.lproj/StreamEncryption.storyboard @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Examples/Advanced/StreamEncryption/StreamEncryption.swift b/macOS/APIExample/Examples/Advanced/StreamEncryption/StreamEncryption.swift new file mode 100644 index 000000000..72be49896 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/StreamEncryption/StreamEncryption.swift @@ -0,0 +1,391 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import Cocoa +import AgoraRtcKit +import AGEVideoLayout + +class StreamEncryption: BaseViewController { + var videos: [VideoView] = [] + + var agoraKit: AgoraRtcEngineKit! + + @IBOutlet weak var Container: AGEVideoContainer! + + /** + --- Resolutions Picker --- + */ + @IBOutlet weak var selectResolutionPicker: Picker! + var selectedResolution: Resolution? { + let index = self.selectResolutionPicker.indexOfSelectedItem + if index >= 0 && index < Configs.Resolutions.count { + return Configs.Resolutions[index] + } else { + return nil + } + } + func initSelectResolutionPicker() { + selectResolutionPicker.label.stringValue = "Resolution".localized + selectResolutionPicker.picker.addItems(withTitles: Configs.Resolutions.map { $0.name() }) + selectResolutionPicker.picker.selectItem(at: GlobalSettings.shared.resolutionSetting.selectedOption().value) + + selectResolutionPicker.onSelectChanged { + if !self.isJoined { + return + } + + guard let resolution = self.selectedResolution, + let fps = self.selectedFps else { + return + } + self.agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + } + } + + /** + --- Fps Picker --- + */ + @IBOutlet weak var selectFpsPicker: Picker! + var selectedFps: Int? { + let index = self.selectFpsPicker.indexOfSelectedItem + if index >= 0 && index < Configs.Fps.count { + return Configs.Fps[index] + } else { + return nil + } + } + func initSelectFpsPicker() { + selectFpsPicker.label.stringValue = "Frame Rate".localized + selectFpsPicker.picker.addItems(withTitles: Configs.Fps.map { "\($0)fps" }) + selectFpsPicker.picker.selectItem(at: GlobalSettings.shared.fpsSetting.selectedOption().value) + + selectFpsPicker.onSelectChanged { + if !self.isJoined { + return + } + guard let resolution = self.selectedResolution, + let fps = self.selectedFps else { + return + } + self.agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + } + } + + /** + --- Layout Picker --- + */ + @IBOutlet weak var selectLayoutPicker: Picker! + let layouts = [Layout("1v1", 2), Layout("1v3", 4), Layout("1v8", 9), Layout("1v15", 16)] + var selectedLayout: Layout? { + let index = self.selectLayoutPicker.indexOfSelectedItem + if index >= 0 && index < layouts.count { + return layouts[index] + } else { + return nil + } + } + func initSelectLayoutPicker() { + layoutVideos(2) + selectLayoutPicker.label.stringValue = "Layout".localized + selectLayoutPicker.picker.addItems(withTitles: layouts.map { $0.label }) + selectLayoutPicker.onSelectChanged { + if self.isJoined { + return + } + guard let layout = self.selectedLayout else { return } + self.layoutVideos(layout.value) + } + } + + /** + --- Encryption Picker --- + */ + @IBOutlet weak var selectEncryptionPicker: Picker! + var encrptions = AgoraEncryptionMode.allValues() + var selectedEncrption: AgoraEncryptionMode? { + let index = self.selectEncryptionPicker.indexOfSelectedItem + if index >= 0 && index < encrptions.count { + return encrptions[index] + } else { + return nil + } + } + func initSelectEncryptionPicker() { + selectEncryptionPicker.label.stringValue = "Encryption Mode".localized + selectEncryptionPicker.picker.addItems(withTitles: encrptions.map { $0.description() }) + selectEncryptionPicker.picker.addItem(withTitle: "Custom") + } + + /** + --- Channel TextField --- + */ + @IBOutlet weak var channelField: Input! + func initChannelField() { + channelField.label.stringValue = "Channel".localized + channelField.field.placeholderString = "Channel Name".localized + } + + /** + --- Channel TextField --- + */ + @IBOutlet weak var encryptionSecretField: Input! + func initEncryptionSecretField() { + encryptionSecretField.label.stringValue = "Encryption Secret".localized + encryptionSecretField.field.placeholderString = "Input Encryption Secret".localized + } + + /** + --- Button --- + */ + @IBOutlet weak var joinChannelButton: NSButton! + func initJoinChannelButton() { + joinChannelButton.title = isJoined ? "Leave Channel".localized : "Join Channel".localized + } + + // indicate if current instance has joined channel + var isJoined: Bool = false { + didSet { + channelField.isEnabled = !isJoined + encryptionSecretField.isEnabled = !isJoined + selectLayoutPicker.isEnabled = !isJoined + selectEncryptionPicker.isEnabled = !isJoined + initJoinChannelButton() + } + } + + // indicate for doing something + var isProcessing: Bool = false { + didSet { + joinChannelButton.isEnabled = !isProcessing + } + } + + override func viewDidLoad() { + super.viewDidLoad() + // Do view setup here. + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + agoraKit.enableVideo() + + initSelectResolutionPicker() + initSelectFpsPicker() + initSelectLayoutPicker() + initSelectEncryptionPicker() + initChannelField() + initEncryptionSecretField() + initJoinChannelButton() + } + + override func viewWillBeRemovedFromSplitView() { + if isJoined { + agoraKit.disableVideo() + // deregister your own custom algorithm encryption + AgoraCustomEncryption.deregisterPacketProcessing(agoraKit) + agoraKit.leaveChannel { (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + } + } + } + + @IBAction func onJoinPressed(_ sender:Any) { + if !isJoined { + // check configuration + let channel = channelField.stringValue + if channel.isEmpty { + return + } + guard let resolution = selectedResolution, + let fps = selectedFps else { + return + } + agoraKit.enableVideo() + // set live broadcaster mode + agoraKit.setChannelProfile(.liveBroadcasting) + // set myself as broadcaster to stream video/audio + agoraKit.setClientRole(.broadcaster) + // enable video module and set up video encoding configs + agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + // enable encryption + let useCustom = selectEncryptionPicker.picker.selectedItem?.title == "Custom" + if !useCustom && selectedEncrption != nil { + // sdk encryption + let config = AgoraEncryptionConfig() + config.encryptionMode = selectedEncrption! + config.encryptionKey = encryptionSecretField.stringValue + let ret = agoraKit.enableEncryption(true, encryptionConfig: config) + if ret != 0 { + // for errors please take a look at: + // CN https://docs.agora.io/cn/Video/API%20Reference/oc/Classes/AgoraRtcEngineKit.html#//api/name/enableEncryption:encryptionConfig: + // EN https://docs.agora.io/en/Video/API%20Reference/oc/Classes/AgoraRtcEngineKit.html#//api/name/enableEncryption:encryptionConfig: + self.showAlert(title: "Error", message: "enableEncryption call failed: \(ret), please check your params") + } + // set up local video to render your local camera preview + let localVideo = videos[0] + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + // the view to be binded + videoCanvas.view = localVideo.videocanvas + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + isProcessing = true + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channel, info: nil, uid: 0, options: option) + if result != 0 { + isProcessing = false + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } else { + // your own custom algorithm encryption + AgoraCustomEncryption.registerPacketProcessing(agoraKit) + } + } else { + isProcessing = true + agoraKit.disableVideo() + agoraKit.leaveChannel { [unowned self] (stats:AgoraChannelStats) in + self.isProcessing = false + LogUtils.log(message: "Left channel", level: .info) + self.videos[0].uid = nil + self.isJoined = false + self.videos.forEach { + $0.uid = nil + $0.statsLabel.stringValue = "" + } + } + } + } + + func layoutVideos(_ count: Int) { + videos = [] + for i in 0...count - 1 { + let view = VideoView.createFromNib()! + if(i == 0) { + view.placeholder.stringValue = "Local" + } else { + view.placeholder.stringValue = "Remote \(i)" + } + videos.append(view) + } + // layout render view + Container.layoutStream(views: videos) + } +} + +/// agora rtc engine delegate events +extension StreamEncryption: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + if isProcessing { + isProcessing = false + } + self.showAlert(title: "Error", message: "Error \(errorCode.rawValue) occur") + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isProcessing = false + isJoined = true + let localVideo = videos[0] + localVideo.uid = uid + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // find a VideoView w/o uid assigned + if let remoteVideo = videos.first(where: { $0.uid == nil }) { + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videocanvas + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + remoteVideo.uid = uid + } else { + LogUtils.log(message: "no video canvas available for \(uid), cancel bind", level: .warning) + } + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + if let remoteVideo = videos.first(where: { $0.uid == uid }) { + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + remoteVideo.uid = nil + } else { + LogUtils.log(message: "no matching video canvas for \(uid), cancel unbind", level: .warning) + } + } +} diff --git a/macOS/APIExample/Examples/Advanced/StreamEncryption/zh-Hans.lproj/StreamEncryption.strings b/macOS/APIExample/Examples/Advanced/StreamEncryption/zh-Hans.lproj/StreamEncryption.strings new file mode 100644 index 000000000..a3003f84a --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/StreamEncryption/zh-Hans.lproj/StreamEncryption.strings @@ -0,0 +1,27 @@ + +/* Class = "NSButtonCell"; title = "Join"; ObjectID = "1ik-om-mWj"; */ +"1ik-om-mWj.title" = "加入频道"; + +/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "6f9-0B-egB"; */ +"6f9-0B-egB.title" = "1V1"; + +/* Class = "NSViewController"; title = "Stream Encryption"; ObjectID = "Gwp-vd-c2J"; */ +"Gwp-vd-c2J.title" = "Stream Encryption"; + +/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "Owt-vb-7U9"; */ +"Owt-vb-7U9.title" = "离开频道"; + +/* Class = "NSMenuItem"; title = "1V3"; ObjectID = "S4i-eh-YzK"; */ +"S4i-eh-YzK.title" = "1V3"; + +/* Class = "NSTextFieldCell"; placeholderString = "加入频道"; ObjectID = "aj5-Fn-je9"; */ +"aj5-Fn-je9.placeholderString" = "输入频道名"; + +/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "cxo-X2-S8L"; */ +"cxo-X2-S8L.title" = "1V15"; + +/* Class = "NSTextFieldCell"; placeholderString = "Encryption Secret"; ObjectID = "sOM-VA-bwW"; */ +"sOM-VA-bwW.placeholderString" = "加密密码"; + +/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "zu1-vg-leG"; */ +"zu1-vg-leG.title" = "1V8"; diff --git a/macOS/APIExample/Examples/Advanced/VoiceChanger/Base.lproj/VoiceChanger.storyboard b/macOS/APIExample/Examples/Advanced/VoiceChanger/Base.lproj/VoiceChanger.storyboard new file mode 100644 index 000000000..2bcf778d2 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/VoiceChanger/Base.lproj/VoiceChanger.storyboard @@ -0,0 +1,582 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Examples/Advanced/VoiceChanger/VoiceChanger.swift b/macOS/APIExample/Examples/Advanced/VoiceChanger/VoiceChanger.swift new file mode 100644 index 000000000..036fa9688 --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/VoiceChanger/VoiceChanger.swift @@ -0,0 +1,747 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import Cocoa +import AgoraRtcKit +import AGEVideoLayout + +class VoiceChanger: BaseViewController { + class PickerProps { + let min: T + let max: T + var value: T + init(min: T, max: T, defaultValue: T) { + self.min = min + self.max = max + self.value = defaultValue + } + } + struct VoiceChangerOption { + var beautifierPreset: AgoraVoiceBeautifierPreset? + var effectPreset: AgoraAudioEffectPreset? + + init() {} + + init(beautifierPreset:AgoraVoiceBeautifierPreset) { + self.beautifierPreset = beautifierPreset + } + + init(effectPreset:AgoraAudioEffectPreset) { + self.effectPreset = effectPreset + } + + func description() -> String { + if let beautifierPreset = self.beautifierPreset { + return beautifierPreset.description() + } + if let effectPreset = self.effectPreset { + return effectPreset.description() + } + return "Off".localized + } + } + + var videos: [VideoView] = [] + + @IBOutlet weak var container: AGEVideoContainer! + + var agoraKit: AgoraRtcEngineKit! + + /** + --- Microphones Picker --- + */ + @IBOutlet weak var selectMicsPicker: Picker! + var mics:[AgoraRtcDeviceInfo] = [] { + didSet { + DispatchQueue.main.async {[unowned self] in + self.selectMicsPicker.picker.addItems(withTitles: self.mics.map {$0.deviceName ?? "unknown"}) + } + } + } + var selectedMicrophone: AgoraRtcDeviceInfo? { + let index = self.selectMicsPicker.indexOfSelectedItem + if index >= 0 && index < mics.count { + return mics[index] + } else { + return nil + } + } + func initSelectMicsPicker() { + selectMicsPicker.label.stringValue = "Microphone".localized + // find device in a separate thread to avoid blocking main thread + let queue = DispatchQueue(label: "device.enumerateDevices") + queue.async {[unowned self] in + self.mics = self.agoraKit.enumerateDevices(.audioRecording) ?? [] + } + + selectMicsPicker.onSelectChanged { + if !self.isJoined { + return + } + // use selected devices + guard let micId = self.selectedMicrophone?.deviceId else { + return + } + self.agoraKit.setDevice(.audioRecording, deviceId: micId) + } + } + + /** + --- Layout Picker --- + */ + @IBOutlet weak var selectLayoutPicker: Picker! + let layouts = [Layout("1v1", 2), Layout("1v3", 4), Layout("1v8", 9), Layout("1v15", 16)] + var selectedLayout: Layout? { + let index = self.selectLayoutPicker.indexOfSelectedItem + if index >= 0 && index < layouts.count { + return layouts[index] + } else { + return nil + } + } + func initSelectLayoutPicker() { + layoutVideos(2) + selectLayoutPicker.label.stringValue = "Layout".localized + selectLayoutPicker.picker.addItems(withTitles: layouts.map { $0.label }) + selectLayoutPicker.onSelectChanged { + if self.isJoined { + return + } + guard let layout = self.selectedLayout else { return } + self.layoutVideos(layout.value) + } + } + + var currentAudioEffects:AgoraAudioEffectPreset = .audioEffectOff + + func updateAudioEffectsControls(_ effect: AgoraAudioEffectPreset?) { + if let _effect = effect { + currentAudioEffects = _effect + switch effect { + case .roomAcoustics3DVoice: + updateInput(field: audioEffectParam1Field, isEnable: true, label: "Cycle(0-60)".localized, value: 10) + updateInput(field: audioEffectParam2Field, isEnable: false) + audioEffectBtn.isEnabled = true + case .pitchCorrection: + updateInput(field: audioEffectParam1Field, isEnable: true, label: "Tonic Mode(1-3)".localized, value: 1) + updateInput(field: audioEffectParam2Field, isEnable: true, label: "Tonic Pitch(1-12)".localized, value: 4) + audioEffectBtn.isEnabled = true + default: + updateInput(field: audioEffectParam1Field, isEnable: false) + updateInput(field: audioEffectParam2Field, isEnable: false) + audioEffectBtn.isEnabled = false + } + } else { + currentAudioEffects = .audioEffectOff + updateInput(field: audioEffectParam1Field, isEnable: false) + updateInput(field: audioEffectParam2Field, isEnable: false) + audioEffectBtn.isEnabled = false + } + } + /** + --- chat Beautifier Picker --- + */ + @IBOutlet weak var selectChatBeautifierPicker: Picker! + let chatBeautifiers: [VoiceChangerOption] = [VoiceChangerOption(), VoiceChangerOption(beautifierPreset:.chatBeautifierFresh), VoiceChangerOption(beautifierPreset:.chatBeautifierVitality), VoiceChangerOption(beautifierPreset:.chatBeautifierMagnetic)] + var selectedChatBeautifier: VoiceChangerOption? { + let index = self.selectChatBeautifierPicker.indexOfSelectedItem + if index >= 0 && index < chatBeautifiers.count { + return chatBeautifiers[index] + } else { + return nil + } + } + func initSelectChatBeautifierPicker() { + selectChatBeautifierPicker.isEnabled = false + selectChatBeautifierPicker.label.stringValue = "Chat Beautifier".localized + selectChatBeautifierPicker.picker.addItems(withTitles: chatBeautifiers.map { $0.description() }) + selectChatBeautifierPicker.onSelectChanged { + if !self.isJoined { + return + } + guard let option = self.selectedChatBeautifier else { return } + self.updateVoiceChangerOption(sender: self.selectChatBeautifierPicker.picker, option: option) + } + } + + /** + --- Timbre Transformation Picker --- + */ + @IBOutlet weak var selectTimbreTransformationPicker: Picker! + let timbreTransformations: [VoiceChangerOption] = [VoiceChangerOption(), VoiceChangerOption(beautifierPreset:.timbreTransformationVigorous), VoiceChangerOption(beautifierPreset:.timbreTransformationDeep), VoiceChangerOption(beautifierPreset:.timbreTransformationMellow), VoiceChangerOption(beautifierPreset:.timbreTransformationFalsetto), VoiceChangerOption(beautifierPreset:.timbreTransformationFull), VoiceChangerOption(beautifierPreset:.timbreTransformationClear), VoiceChangerOption(beautifierPreset:.timbreTransformationResounding), VoiceChangerOption(beautifierPreset:.timbreTransformationRinging)] + var selectedTimbreTransformation: VoiceChangerOption? { + let index = self.selectTimbreTransformationPicker.indexOfSelectedItem + if index >= 0 && index < timbreTransformations.count { + return timbreTransformations[index] + } else { + return nil + } + } + func initSelectTimbreTransformationPicker() { + selectTimbreTransformationPicker.isEnabled = false + selectTimbreTransformationPicker.label.stringValue = "Timbre Transformation".localized + selectTimbreTransformationPicker.picker.addItems(withTitles: timbreTransformations.map { $0.description() }) + selectTimbreTransformationPicker.onSelectChanged { + if !self.isJoined { + return + } + guard let option = self.selectedTimbreTransformation else { return } + self.updateVoiceChangerOption(sender: self.selectTimbreTransformationPicker.picker, option: option) + } + } + + /** + --- Voice Changer Picker --- + */ + @IBOutlet weak var selectVoiceChangerPicker: Picker! + let voiceChangers: [VoiceChangerOption] = [VoiceChangerOption(), VoiceChangerOption(effectPreset:.voiceChangerEffectOldMan), VoiceChangerOption(effectPreset:.voiceChangerEffectBoy), VoiceChangerOption(effectPreset:.voiceChangerEffectGirl), VoiceChangerOption(effectPreset:.voiceChangerEffectPigKing), VoiceChangerOption(effectPreset:.voiceChangerEffectHulk), VoiceChangerOption(effectPreset:.voiceChangerEffectUncle), VoiceChangerOption(effectPreset:.voiceChangerEffectSister)] + var selectedVoiceChanger: VoiceChangerOption? { + let index = self.selectVoiceChangerPicker.indexOfSelectedItem + if index >= 0 && index < voiceChangers.count { + return voiceChangers[index] + } else { + return nil + } + } + func initSelectVoiceChangerPicker() { + selectVoiceChangerPicker.isEnabled = false + selectVoiceChangerPicker.label.stringValue = "Voice Changer".localized + selectVoiceChangerPicker.picker.addItems(withTitles: voiceChangers.map { $0.description() }) + selectVoiceChangerPicker.onSelectChanged { + if !self.isJoined { + return + } + guard let option = self.selectedVoiceChanger else { return } + self.updateVoiceChangerOption(sender: self.selectVoiceChangerPicker.picker, option: option) + } + } + + /** + -- style Transformation Picker -- + */ + @IBOutlet weak var selectStyleTransformationPicker: Picker! + let styleTransformations: [VoiceChangerOption] = [VoiceChangerOption(), VoiceChangerOption(effectPreset:.styleTransformationPopular), VoiceChangerOption(effectPreset:.styleTransformationRnB)] + var selectedStyleTransformation: VoiceChangerOption? { + let index = self.selectVoiceChangerPicker.indexOfSelectedItem + if index >= 0 && index < styleTransformations.count { + return styleTransformations[index] + } else { + return nil + } + } + func initSelectStyleTransformationPicker() { + selectStyleTransformationPicker.isEnabled = false + selectStyleTransformationPicker.label.stringValue = "Style Transformation".localized + selectStyleTransformationPicker.picker.addItems(withTitles: styleTransformations.map { $0.description() }) + selectStyleTransformationPicker.onSelectChanged { + if !self.isJoined { + return + } + guard let option = self.selectedStyleTransformation else { return } + self.updateVoiceChangerOption(sender: self.selectStyleTransformationPicker.picker, option: option) + } + } + + /** + --- room Acoustics Picker --- + */ + @IBOutlet weak var selectRoomAcousticsPicker: Picker! + let roomAcoustics: [VoiceChangerOption] = [VoiceChangerOption(), VoiceChangerOption(effectPreset:.roomAcousticsSpacial), VoiceChangerOption(effectPreset:.roomAcousticsEthereal), VoiceChangerOption(effectPreset:.roomAcousticsVocalConcert), VoiceChangerOption(effectPreset:.roomAcousticsKTV), VoiceChangerOption(effectPreset:.roomAcousticsStudio), VoiceChangerOption(effectPreset:.roomAcousticsPhonograph), VoiceChangerOption(effectPreset:.roomAcousticsVirtualStereo), VoiceChangerOption(effectPreset:.roomAcoustics3DVoice)] + var selectedRoomAcoustics: VoiceChangerOption? { + let index = self.selectRoomAcousticsPicker.indexOfSelectedItem + if index >= 0 && index < roomAcoustics.count { + return roomAcoustics[index] + } else { + return nil + } + } + func initSelectRoomAcousticsPicker() { + selectRoomAcousticsPicker.isEnabled = false + selectRoomAcousticsPicker.label.stringValue = "Room Acoustics".localized + selectRoomAcousticsPicker.picker.addItems(withTitles: roomAcoustics.map { $0.description() }) + selectRoomAcousticsPicker.onSelectChanged { + if !self.isJoined { + return + } + guard let option = self.selectedRoomAcoustics else { return } + self.updateVoiceChangerOption(sender: self.selectRoomAcousticsPicker.picker, option: option) + } + } + + /** + --- pitch Correction Picker --- + */ + @IBOutlet weak var selectPitchCorrectionPicker: Picker! + let pitchCorrections: [VoiceChangerOption] = [VoiceChangerOption(), VoiceChangerOption(effectPreset:.pitchCorrection)] + var selectedPitchCorrection: VoiceChangerOption? { + let index = self.selectPitchCorrectionPicker.indexOfSelectedItem + if index >= 0 && index < pitchCorrections.count { + return pitchCorrections[index] + } else { + return nil + } + } + func initSelectPitchCorrectionPicker() { + selectPitchCorrectionPicker.isEnabled = false + selectPitchCorrectionPicker.label.stringValue = "Pitch Correction".localized + selectPitchCorrectionPicker.picker.addItems(withTitles: pitchCorrections.map { $0.description() }) + selectPitchCorrectionPicker.onSelectChanged { + if !self.isJoined { + return + } + guard let option = self.selectedPitchCorrection else { return } + self.updateVoiceChangerOption(sender: self.selectPitchCorrectionPicker.picker, option: option) + } + } + + /** + --- set audio effect button --- + */ + @IBOutlet weak var audioEffectBtn: NSButton! + func initAudioEffectButton() { + audioEffectBtn.title = "Set Audio Effect Params".localized + } + @IBAction func onAudioEffectParamsUpdate(_ sender: NSButton) { + let param1 = audioEffectParam1Field.isEnabled ? audioEffectParam1Field.field.intValue : 0 + let param2 = audioEffectParam2Field.isEnabled ? audioEffectParam2Field.field.intValue : 0 + LogUtils.log(message: "onAudioEffectsParamUpdated \(currentAudioEffects.description()) \(param1) \(param2)", level: .info) + agoraKit.setAudioEffectParameters(currentAudioEffects, param1: param1, param2: param2) + } + + func updateInput(field: Input, isEnable: Bool, label: String = "N/A", value: Int32 = 0) { + field.isEnabled = isEnable + field.label.stringValue = label + field.field.intValue = value + } + /** + --- audio effice param1 --- + */ + @IBOutlet weak var audioEffectParam1Field: Input! + func initAudioEffectParam1Field() { + updateInput(field: audioEffectParam1Field, isEnable: false) + } + + /** + --- audio effice param2 --- + */ + @IBOutlet weak var audioEffectParam2Field: Input! + func initAudioEffectParam2Field() { + updateInput(field: audioEffectParam2Field, isEnable: false) + } + + /** + --- equalization Reverb Key Picker --- + */ + @IBOutlet weak var equalizationReverbKeyPicker: NSPopUpButton! + var reverbMap: [AgoraAudioReverbType: PickerProps] = [ + .dryLevel: PickerProps(min: -20, max: 10, defaultValue: 0), + .wetLevel: PickerProps(min: -20, max: 10, defaultValue: 0), + .roomSize: PickerProps(min: 0, max: 100, defaultValue: 0), + .wetDelay: PickerProps(min: 0, max: 200, defaultValue: 0), + .strength: PickerProps(min: 0, max: 100, defaultValue: 0) + ] + let equalizationReverbKeys: [AgoraAudioReverbType] = [.dryLevel, .wetLevel, .roomSize, .wetDelay, .strength] + var selectedEqualizationReverbKey: AgoraAudioReverbType? { + let index = self.equalizationReverbKeyPicker.indexOfSelectedItem + if index >= 0 && index < equalizationReverbKeys.count { + return equalizationReverbKeys[index] + } else { + return nil + } + } + func initEqualizationReverbKeyPicker() { + equalizationReverbKeyPicker.addItems(withTitles: equalizationReverbKeys.map { $0.description() }) + } + @IBAction func onLocalVoiceEqualizationReverbKey(_ sender: NSPopUpButton) { + guard let reverbType = selectedEqualizationReverbKey, + let props = reverbMap[reverbType] else { return } + equalizationReverbValueSlider.minValue = props.min + equalizationReverbValueSlider.maxValue = props.max + equalizationReverbValueSlider.doubleValue = props.value + } + /** + --- equalizationReverbValue Slider --- + */ + @IBOutlet weak var equalizationReverbValueSlider: NSSlider! + @IBAction func onLocalVoiceReverbValue(_ sender:NSSlider) { + guard let reverbType = selectedEqualizationReverbKey, + let props = reverbMap[reverbType] else { return } + let value = Int(sender.doubleValue) + props.value = Double(sender.intValue) + LogUtils.log(message: "onLocalVoiceReverbValue \(reverbType.description()) \(value)", level: .info) + agoraKit.setLocalVoiceReverbOf(reverbType, withValue: value) + } + + /** + --- Voice Pitch Slider --- + */ + @IBOutlet weak var voicePitchSlider: Slider! + func initVoicePitchSlider() { + voicePitchSlider.isEnabled = false + voicePitchSlider.label.stringValue = "Voice Pitch".localized + voicePitchSlider.slider.minValue = 0.5 + voicePitchSlider.slider.maxValue = 2.0 + voicePitchSlider.slider.doubleValue = 1.0 + + voicePitchSlider.onSliderChanged { + LogUtils.log(message: "onLocalVoicePitch \(self.voicePitchSlider.slider.doubleValue)", level: .info) + self.agoraKit.setLocalVoicePitch(self.voicePitchSlider.slider.doubleValue) + } + } + + @IBOutlet weak var equalization31hzPicker: NSSlider! + @IBOutlet weak var equalization62hzPicker: NSSlider! + @IBOutlet weak var equalization125hzPicker: NSSlider! + @IBOutlet weak var equalization250hzPicker: NSSlider! + @IBOutlet weak var equalization500hzPicker: NSSlider! + @IBOutlet weak var equalization1khzPicker: NSSlider! + @IBOutlet weak var equalization2khzPicker: NSSlider! + @IBOutlet weak var equalization4khzPicker: NSSlider! + @IBOutlet weak var equalization8khzPicker: NSSlider! + @IBOutlet weak var equalization16khzPicker: NSSlider! + + /** + --- Channel TextField --- + */ + @IBOutlet weak var channelField: Input! + func initChannelField() { + channelField.label.stringValue = "Channel".localized + channelField.field.placeholderString = "Channel Name".localized + } + + /** + --- Button --- + */ + @IBOutlet weak var joinCHannelButton: NSButton! + func initJoinChannelButton() { + joinCHannelButton.title = isJoined ? "Leave Channel".localized : "Join Channel".localized + } + + // indicate if current instance has joined channel + var isJoined: Bool = false { + didSet { + channelField.isEnabled = !isJoined + selectLayoutPicker.isEnabled = !isJoined + initJoinChannelButton() + selectChatBeautifierPicker.isEnabled = isJoined + selectTimbreTransformationPicker.isEnabled = isJoined + selectVoiceChangerPicker.isEnabled = isJoined + selectStyleTransformationPicker.isEnabled = isJoined + selectRoomAcousticsPicker.isEnabled = isJoined + selectPitchCorrectionPicker.isEnabled = isJoined + voicePitchSlider.isEnabled = isJoined + equalization31hzPicker.isEnabled = isJoined + equalization62hzPicker.isEnabled = isJoined + equalization125hzPicker.isEnabled = isJoined + equalization250hzPicker.isEnabled = isJoined + equalization500hzPicker.isEnabled = isJoined + equalization1khzPicker.isEnabled = isJoined + equalization2khzPicker.isEnabled = isJoined + equalization4khzPicker.isEnabled = isJoined + equalization8khzPicker.isEnabled = isJoined + equalization16khzPicker.isEnabled = isJoined + equalizationReverbKeyPicker.isEnabled = isJoined + equalizationReverbValueSlider.isEnabled = isJoined + if !isJoined { + updateAudioEffectsControls(nil) + } + } + } + + // indicate for doing something + var isProcessing: Bool = false { + didSet { + joinCHannelButton.isEnabled = !isProcessing + } + } + + override func viewDidLoad() { + super.viewDidLoad() + // set up agora instance when view loaded + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + initSelectMicsPicker() + initSelectLayoutPicker() + initSelectChatBeautifierPicker() + initSelectTimbreTransformationPicker() + initSelectVoiceChangerPicker() + initSelectStyleTransformationPicker() + initSelectRoomAcousticsPicker() + initSelectPitchCorrectionPicker() + initAudioEffectParam1Field() + initAudioEffectParam2Field() + initAudioEffectButton() + initEqualizationReverbKeyPicker() + initVoicePitchSlider() + + initChannelField() + initJoinChannelButton() + } + + override func viewWillBeRemovedFromSplitView() { + if isJoined { + agoraKit.leaveChannel { (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + } + } + } + + @IBAction func onJoinPressed(_ sender:Any) { + if !isJoined { + // check configuration + let channel = channelField.stringValue + if channel.isEmpty { + return + } + // use selected devices + guard let micId = selectedMicrophone?.deviceId else { + return + } + agoraKit.setDevice(.audioRecording, deviceId: micId) + // disable video module in audio scene + agoraKit.disableVideo() + // Before calling the method, you need to set the profile + // parameter of setAudioProfile to AUDIO_PROFILE_MUSIC_HIGH_QUALITY(4) + // or AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO(5), and to set + // scenario parameter to AUDIO_SCENARIO_GAME_STREAMING(3). + agoraKit.setAudioProfile(.musicHighQualityStereo, scenario: .gameStreaming) + + // set live broadcaster mode + agoraKit.setChannelProfile(.liveBroadcasting) + // set myself as broadcaster to stream audio + agoraKit.setClientRole(.broadcaster) + + // enable volume indicator + agoraKit.enableAudioVolumeIndication(200, smooth: 3, report_vad: false) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + isProcessing = true + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channel, info: nil, uid: 0, options: option) + if result != 0 { + isProcessing = false + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } else { + isProcessing = true + agoraKit.leaveChannel { [unowned self] (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + self.isProcessing = false + self.videos[0].uid = nil + self.isJoined = false + } + } + } + + @IBAction func onBand31hz(_ sender: NSSlider) { + updateVoiceBand(frequency: .band31, gain: Int(sender.doubleValue)) + } + + @IBAction func onBand62hz(_ sender: NSSlider) { + updateVoiceBand(frequency: .band62, gain: Int(sender.doubleValue)) + } + + @IBAction func onBand125hz(_ sender: NSSlider) { + updateVoiceBand(frequency: .band125, gain: Int(sender.doubleValue)) + } + + @IBAction func onBand250hz(_ sender: NSSlider) { + updateVoiceBand(frequency: .band250, gain: Int(sender.doubleValue)) + } + + @IBAction func onBand500hz(_ sender: NSSlider) { + updateVoiceBand(frequency: .band500, gain: Int(sender.doubleValue)) + } + + @IBAction func onBand1khz(_ sender: NSSlider) { + updateVoiceBand(frequency: .band1K, gain: Int(sender.doubleValue)) + } + + @IBAction func onBand2khz(_ sender: NSSlider) { + updateVoiceBand(frequency: .band2K, gain: Int(sender.doubleValue)) + } + + @IBAction func onBand4khz(_ sender: NSSlider) { + updateVoiceBand(frequency: .band4K, gain: Int(sender.doubleValue)) + } + + @IBAction func onBand8khz(_ sender: NSSlider) { + updateVoiceBand(frequency: .band8K, gain: Int(sender.doubleValue)) + } + + @IBAction func onBand16khz(_ sender: NSSlider) { + updateVoiceBand(frequency: .band16K, gain: Int(sender.doubleValue)) + } + + func updateVoiceBand(frequency:AgoraAudioEqualizationBandFrequency, gain:Int) { + LogUtils.log(message: "setLocalVoiceEqualization: \(frequency.description()), gain: \(gain)", level: .info) + agoraKit.setLocalVoiceEqualizationOf(frequency, withGain: gain) + } + + func updateVoiceChangerOption(sender: NSPopUpButton, option: VoiceChangerOption) { + let pickers = [ + selectChatBeautifierPicker.picker, + selectTimbreTransformationPicker.picker, + selectVoiceChangerPicker.picker, + selectStyleTransformationPicker.picker, + selectRoomAcousticsPicker.picker + ] + pickers.filter { + $0 != sender + }.forEach { + $0?.selectItem(at: 0) + } + + if let beautifierPreset = option.beautifierPreset { + LogUtils.log(message: "setVoiceBeautifierPreset: \(beautifierPreset.description())", level: .info) + agoraKit.setVoiceBeautifierPreset(beautifierPreset) + updateAudioEffectsControls(nil) + } else if let effectPreset = option.effectPreset { + LogUtils.log(message: "setAudioEffectPreset: \(effectPreset.description())", level: .info) + updateAudioEffectsControls(effectPreset) + agoraKit.setAudioEffectPreset(effectPreset) + } else { + // turn off if it's an off option + agoraKit.setVoiceBeautifierPreset(.voiceBeautifierOff) + agoraKit.setAudioEffectPreset(.audioEffectOff) + updateAudioEffectsControls(nil) + } + } + + func layoutVideos(_ count: Int) { + videos = [] + for i in 0...count - 1 { + let view = VideoView.createFromNib()! + if(i == 0) { + view.placeholder.stringValue = "Local" + view.type = .local + view.statsInfo = StatisticsInfo(type: .local(StatisticsInfo.LocalInfo())) + } else { + view.placeholder.stringValue = "Remote \(i)" + view.type = .remote + view.statsInfo = StatisticsInfo(type: .remote(StatisticsInfo.RemoteInfo())) + } + view.audioOnly = true + videos.append(view) + } + // layout render view + container.layoutStream(views: videos) + } +} + +/// agora rtc engine delegate events +extension VoiceChanger: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + if isProcessing { + isProcessing = false + } + self.showAlert(title: "Error", message: "Error \(errorCode.rawValue) occur") + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isProcessing = false + isJoined = true + let localVideo = videos[0] + localVideo.uid = uid + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // find a VideoView w/o uid assigned + if let remoteVideo = videos.first(where: { $0.uid == nil }) { + remoteVideo.uid = uid + } else { + LogUtils.log(message: "no video canvas available for \(uid), cancel bind", level: .warning) + } + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + if let remoteVideo = videos.first(where: { $0.uid == uid }) { + remoteVideo.uid = nil + } else { + LogUtils.log(message: "no matching video canvas for \(uid), cancel unbind", level: .warning) + } + } + + /// Reports the statistics of the current call. The SDK triggers this callback once every two seconds after the user joins the channel. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, reportRtcStats stats: AgoraChannelStats) { + videos[0].statsInfo?.updateChannelStats(stats) + } + + /// Reports the statistics of the uploading local audio streams once every two seconds. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, localAudioStats stats: AgoraRtcLocalAudioStats) { + videos[0].statsInfo?.updateLocalAudioStats(stats) + } + + /// Reports the statistics of the audio stream from each remote user/host. + /// @param stats stats struct for current call statistics + func rtcEngine(_ engine: AgoraRtcEngineKit, remoteAudioStats stats: AgoraRtcRemoteAudioStats) { + videos.first(where: { $0.uid == stats.uid })?.statsInfo?.updateAudioStats(stats) + } + + /// Reports which users are speaking, the speakers' volumes, and whether the local user is speaking. + /// @params speakers volume info for all speakers + /// @params totalVolume Total volume after audio mixing. The value range is [0,255]. + func rtcEngine(_ engine: AgoraRtcEngineKit, reportAudioVolumeIndicationOfSpeakers speakers: [AgoraRtcAudioVolumeInfo], totalVolume: Int) { + for volumeInfo in speakers { + if (volumeInfo.uid == 0) { + videos[0].statsInfo?.updateVolume(volumeInfo.volume) + } else { + videos.first(where: { $0.uid == volumeInfo.uid })?.statsInfo?.updateVolume(volumeInfo.volume) + } + } + } +} diff --git a/macOS/APIExample/Examples/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings b/macOS/APIExample/Examples/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings new file mode 100644 index 000000000..a259756be --- /dev/null +++ b/macOS/APIExample/Examples/Advanced/VoiceChanger/zh-Hans.lproj/VoiceChanger.strings @@ -0,0 +1,63 @@ + +/* Class = "NSTextFieldCell"; title = "1Khz"; ObjectID = "5nb-04-vbe"; */ +"5nb-04-vbe.title" = "1Khz"; + +/* Class = "NSBox"; title = "Equalization Reverb"; ObjectID = "5z4-pq-KKl"; */ +"5z4-pq-KKl.title" = "混响调整"; + +/* Class = "NSTextFieldCell"; title = "2Khz"; ObjectID = "6ME-Zv-Hpv"; */ +"6ME-Zv-Hpv.title" = "2Khz"; + +/* Class = "NSTextFieldCell"; title = "250hz"; ObjectID = "8JZ-5R-nCU"; */ +"8JZ-5R-nCU.title" = "250hz"; + +/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "8bV-OK-zbc"; */ +"8bV-OK-zbc.title" = "1V15"; + +/* Class = "NSTextFieldCell"; title = "31hz"; ObjectID = "8fd-8t-Irz"; */ +"8fd-8t-Irz.title" = "31hz"; + +/* Class = "NSTextFieldCell"; title = "16Khz"; ObjectID = "ClO-mY-jZW"; */ +"ClO-mY-jZW.title" = "16Khz"; + +/* Class = "NSTextFieldCell"; placeholderString = "加入频道"; ObjectID = "EhX-UJ-wov"; */ +"EhX-UJ-wov.placeholderString" = "输入频道名"; + +/* Class = "NSMenuItem"; title = "1V3"; ObjectID = "J6a-ul-c2H"; */ +"J6a-ul-c2H.title" = "1V3"; + +/* Class = "NSButtonCell"; title = "Join"; ObjectID = "P4E-oB-5Di"; */ +"P4E-oB-5Di.title" = "加入频道"; + +/* Class = "NSBox"; title = "Equalization Band"; ObjectID = "bBW-s5-1yz"; */ +"bBW-s5-1yz.title" = "波段增益"; + +/* Class = "NSTextFieldCell"; title = "62hz"; ObjectID = "UAW-B9-951"; */ +"UAW-B9-951.title" = "62hz"; + +/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "ch0-OR-L16"; */ +"ch0-OR-L16.title" = "1V1"; + +/* Class = "NSTextFieldCell"; title = "4Khz"; ObjectID = "fUn-bY-2Ur"; */ +"fUn-bY-2Ur.title" = "4Khz"; + +/* Class = "NSTextFieldCell"; title = "500hz"; ObjectID = "gNS-nM-8eg"; */ +"gNS-nM-8eg.title" = "500hz"; + +/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "gWk-wf-hPu"; */ +"gWk-wf-hPu.title" = "1V8"; + +/* Class = "NSTextFieldCell"; title = "125hz"; ObjectID = "iEy-1i-vf4"; */ +"iEy-1i-vf4.title" = "125hz"; + +/* Class = "NSTextFieldCell"; title = "Voice Pitch"; ObjectID = "j8U-Er-3Ry"; */ +"j8U-Er-3Ry.title" = "音调"; + +/* Class = "NSViewController"; title = "Join Channel Audio"; ObjectID = "jAv-ZA-ecf"; */ +"jAv-ZA-ecf.title" = "Join Channel Audio"; + +/* Class = "NSTextFieldCell"; title = "8Khz"; ObjectID = "k68-jy-Jcs"; */ +"k68-jy-Jcs.title" = "8Khz"; + +/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "szu-uz-G6W"; */ +"szu-uz-G6W.title" = "离开频道"; diff --git a/macOS/APIExample/Examples/Basic/JoinChannelAudio/Base.lproj/JoinChannelAudio.storyboard b/macOS/APIExample/Examples/Basic/JoinChannelAudio/Base.lproj/JoinChannelAudio.storyboard new file mode 100644 index 000000000..29745c9bd --- /dev/null +++ b/macOS/APIExample/Examples/Basic/JoinChannelAudio/Base.lproj/JoinChannelAudio.storyboard @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift b/macOS/APIExample/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift new file mode 100644 index 000000000..e3a8cf904 --- /dev/null +++ b/macOS/APIExample/Examples/Basic/JoinChannelAudio/JoinChannelAudio.swift @@ -0,0 +1,487 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import Cocoa +import AgoraRtcKit +import AGEVideoLayout + +class JoinChannelAudioMain: BaseViewController { + + var agoraKit: AgoraRtcEngineKit! + var videos: [VideoView] = [] + @IBOutlet weak var Container: AGEVideoContainer! + + /** + --- Audio Profile Picker --- + */ + @IBOutlet weak var selectAudioProfilePicker: Picker! + var audioProfiles = AgoraAudioProfile.allValues() + var selectedProfile: AgoraAudioProfile? { + let index = selectAudioProfilePicker.indexOfSelectedItem + if index >= 0 && index < audioProfiles.count { + return audioProfiles[index] + } else { + return nil + } + } + func initSelectAudioProfilePicker() { + selectAudioProfilePicker.label.stringValue = "Audio Profile".localized + selectAudioProfilePicker.picker.addItems(withTitles: audioProfiles.map { $0.description() }) + + selectAudioProfilePicker.onSelectChanged { + if !self.isJoined { + return + } + guard let profile = self.selectedProfile, + let scenario = self.selectedAudioScenario else { + return + } + self.agoraKit.setAudioProfile(profile, scenario: scenario) + } + } + + /** + --- Audio Scenario Picker --- + */ + @IBOutlet weak var selectAudioScenarioPicker: Picker! + var audioScenarios = AgoraAudioScenario.allValues() + var selectedAudioScenario: AgoraAudioScenario? { + let index = self.selectAudioScenarioPicker.indexOfSelectedItem + if index >= 0 && index < Configs.Resolutions.count { + return audioScenarios[index] + } else { + return nil + } + } + func initSelectAudioScenarioPicker() { + selectAudioScenarioPicker.label.stringValue = "Audio Scenario".localized + selectAudioScenarioPicker.picker.addItems(withTitles: audioScenarios.map { $0.description() }) + + selectAudioScenarioPicker.onSelectChanged { + if !self.isJoined { + return + } + guard let profile = self.selectedProfile, + let scenario = self.selectedAudioScenario else { + return + } + self.agoraKit.setAudioProfile(profile, scenario: scenario) + } + } + + /** + --- Microphones Picker --- + */ + @IBOutlet weak var selectMicsPicker: Picker! + var mics:[AgoraRtcDeviceInfo] = [] { + didSet { + DispatchQueue.main.async {[unowned self] in + self.selectMicsPicker.picker.addItems(withTitles: self.mics.map {$0.deviceName ?? "unknown"}) + } + } + } + var selectedMicrophone: AgoraRtcDeviceInfo? { + let index = self.selectMicsPicker.indexOfSelectedItem + if index >= 0 && index < mics.count { + return mics[index] + } else { + return nil + } + } + func initSelectMicsPicker() { + selectMicsPicker.label.stringValue = "Microphone".localized + // find device in a separate thread to avoid blocking main thread + let queue = DispatchQueue(label: "device.enumerateDevices") + queue.async {[unowned self] in + self.mics = self.agoraKit.enumerateDevices(.audioRecording) ?? [] + } + + selectMicsPicker.onSelectChanged { + if !self.isJoined { + return + } + // use selected devices + guard let micId = self.selectedMicrophone?.deviceId else { + return + } + self.agoraKit.setDevice(.audioRecording, deviceId: micId) + } + } + + /** + --- Layout Picker --- + */ + @IBOutlet weak var selectLayoutPicker: Picker! + let layouts = [Layout("1v1", 2), Layout("1v3", 4), Layout("1v8", 9), Layout("1v15", 16)] + var selectedLayout: Layout? { + let index = self.selectLayoutPicker.indexOfSelectedItem + if index >= 0 && index < layouts.count { + return layouts[index] + } else { + return nil + } + } + func initSelectLayoutPicker() { + layoutVideos(2) + selectLayoutPicker.label.stringValue = "Layout".localized + selectLayoutPicker.picker.addItems(withTitles: layouts.map { $0.label }) + selectLayoutPicker.onSelectChanged { + if self.isJoined { + return + } + guard let layout = self.selectedLayout else { return } + self.layoutVideos(layout.value) + } + } + + /** + --- Device Recording Volume Slider --- + */ + @IBOutlet weak var deviceRecordingVolumeSlider: Slider! + func initDeviceRecordingVolumeSlider() { + deviceRecordingVolumeSlider.label.stringValue = "Device Recording Volume".localized + deviceRecordingVolumeSlider.slider.minValue = 0 + deviceRecordingVolumeSlider.slider.maxValue = 100 + deviceRecordingVolumeSlider.slider.intValue = 50 + + deviceRecordingVolumeSlider.onSliderChanged { + let volume: Int32 = Int32(self.deviceRecordingVolumeSlider.slider.intValue) + LogUtils.log(message: "onDeviceRecordingVolumeChanged \(volume)", level: .info) + self.agoraKit?.setDeviceVolume(.audioRecording, volume: volume) + } + } + + /** + --- Device Recording Volume Slider --- + */ + @IBOutlet weak var sdkRecordingVolumeSlider: Slider! + func initSdkRecordingVolumeSlider() { + sdkRecordingVolumeSlider.label.stringValue = "SDK Recording Volume".localized + sdkRecordingVolumeSlider.slider.minValue = 0 + sdkRecordingVolumeSlider.slider.maxValue = 100 + sdkRecordingVolumeSlider.slider.intValue = 50 + + sdkRecordingVolumeSlider.onSliderChanged { + let volume: Int = Int(self.sdkRecordingVolumeSlider.slider.intValue) + LogUtils.log(message: "onRecordingVolumeChanged \(volume)", level: .info) + self.agoraKit?.adjustRecordingSignalVolume(volume) + } + } + + /** + --- Device Playout Volume Slider --- + */ + @IBOutlet weak var devicePlayoutVolumeSlider: Slider! + func initDevicePlayoutVolumeSlider() { + devicePlayoutVolumeSlider.label.stringValue = "Device Playout Volume".localized + devicePlayoutVolumeSlider.slider.minValue = 0 + devicePlayoutVolumeSlider.slider.maxValue = 100 + devicePlayoutVolumeSlider.slider.intValue = 50 + + devicePlayoutVolumeSlider.onSliderChanged { + let volume: Int32 = Int32(self.devicePlayoutVolumeSlider.slider.intValue) + LogUtils.log(message: "onDevicePlayoutVolumeChanged \(volume)", level: .info) + self.agoraKit?.setDeviceVolume(.audioPlayout, volume: volume) + } + } + + /** + --- Device Playout Volume Slider --- + */ + @IBOutlet weak var sdkPlaybackVolumeSlider: Slider! + func initSdkPlaybackVolumeSlider() { + sdkPlaybackVolumeSlider.label.stringValue = "SDK Playout Volume".localized + sdkPlaybackVolumeSlider.slider.minValue = 0 + sdkPlaybackVolumeSlider.slider.maxValue = 100 + sdkPlaybackVolumeSlider.slider.intValue = 50 + + sdkPlaybackVolumeSlider.onSliderChanged { + let volume: Int = Int(self.sdkPlaybackVolumeSlider.slider.intValue) + LogUtils.log(message: "onPlaybackVolumeChanged \(volume)", level: .info) + self.agoraKit?.adjustPlaybackSignalVolume(volume) + } + } + + /** + --- Device Playout Volume Slider --- + */ + @IBOutlet weak var firstUserPlaybackVolumeSlider: Slider! + func initFirstUserPlaybackVolumeSlider() { + firstUserPlaybackVolumeSlider.label.stringValue = "User Playback Volume".localized + firstUserPlaybackVolumeSlider.slider.minValue = 0 + firstUserPlaybackVolumeSlider.slider.maxValue = 100 + firstUserPlaybackVolumeSlider.slider.intValue = 50 + setFirstUserPlaybackVolumeSliderEnable() + firstUserPlaybackVolumeSlider.onSliderChanged { + let volume: Int32 = Int32(self.firstUserPlaybackVolumeSlider.slider.intValue) + if self.videos.count > 1 && self.videos[1].uid != nil { + LogUtils.log(message: "onUserPlayoutVolumeChanged \(volume)", level: .info) + self.agoraKit?.adjustUserPlaybackSignalVolume(self.videos[1].uid!, volume: volume) + } + } + } + func setFirstUserPlaybackVolumeSliderEnable() { + firstUserPlaybackVolumeSlider.isEnabled = videos[1].uid != nil + } + + /** + --- Channel TextField --- + */ + @IBOutlet weak var channelField: Input! + func initChannelField() { + channelField.label.stringValue = "Channel".localized + channelField.field.placeholderString = "Channel Name".localized + } + + /** + --- Button --- + */ + @IBOutlet weak var joinChannelButton: NSButton! + func initJoinChannelButton() { + joinChannelButton.title = isJoined ? "Leave Channel".localized : "Join Channel".localized + } + + @IBOutlet weak var localUserSpeaking: NSTextField! + @IBOutlet weak var activeSpeaker: NSTextField! + + // indicate if current instance has joined channel + var isJoined: Bool = false { + didSet { + channelField.isEnabled = !isJoined + selectLayoutPicker.isEnabled = !isJoined + initJoinChannelButton() + } + } + + // indicate for doing something + var isProcessing: Bool = false { + didSet { + joinChannelButton.isEnabled = !isProcessing + } + } + + override func viewDidLoad() { + super.viewDidLoad() + // Do view setup here. + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + + initSelectAudioProfilePicker() + initSelectAudioScenarioPicker() + initSelectMicsPicker() + initSelectLayoutPicker() + + initDeviceRecordingVolumeSlider() + initSdkRecordingVolumeSlider() + initDevicePlayoutVolumeSlider() + initSdkPlaybackVolumeSlider() + initFirstUserPlaybackVolumeSlider() + + initChannelField() + initJoinChannelButton() + } + + func layoutVideos(_ count: Int) { + videos = [] + for i in 0...count - 1 { + let view = VideoView.createFromNib()! + if(i == 0) { + view.placeholder.stringValue = "Local" + view.type = .local + view.statsInfo = StatisticsInfo(type: .local(StatisticsInfo.LocalInfo())) + } else { + view.placeholder.stringValue = "Remote \(i)" + view.type = .remote + view.statsInfo = StatisticsInfo(type: .remote(StatisticsInfo.RemoteInfo())) + } + view.audioOnly = true + videos.append(view) + } + // layout render view + Container.layoutStream(views: videos) + } + + + @IBAction func onJoinButtonPressed(_ sender: NSButton) { + if !isJoined { + // check configuration + let channel = channelField.stringValue + if channel.isEmpty { + return + } + // use selected devices + guard let micId = selectedMicrophone?.deviceId, + let profile = selectedProfile, + let scenario = selectedAudioScenario else { + return + } + agoraKit.setDevice(.audioRecording, deviceId: micId) + // disable video module in audio scene + agoraKit.disableVideo() + agoraKit.enableAudio() + agoraKit.setAudioProfile(profile, scenario: scenario) + + // set live broadcaster mode + agoraKit.setChannelProfile(.liveBroadcasting) + // set myself as broadcaster to stream audio + agoraKit.setClientRole(.broadcaster) + // enable volume indicator + agoraKit.enableAudioVolumeIndication(200, smooth: 3, report_vad: true) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + isProcessing = true + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channel, info: nil, uid: 0, options: option) + if result != 0 { + isProcessing = false + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } else { + isProcessing = true + agoraKit.leaveChannel { (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + self.isProcessing = false + self.videos[0].uid = nil + self.isJoined = false + self.videos.forEach { + $0.uid = nil + $0.statsLabel.stringValue = "" + } + } + } + } + + override func viewWillBeRemovedFromSplitView() { + if isJoined { + agoraKit.leaveChannel { (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + } + } + } +} + +/// agora rtc engine delegate events +extension JoinChannelAudioMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + self.showAlert(title: "Error", message: "Error \(errorCode.rawValue) occur") + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isProcessing = false + isJoined = true + let localVideo = videos[0] + localVideo.uid = uid + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // find a VideoView w/o uid assigned + if let remoteVideo = videos.first(where: { $0.uid == nil }) { + remoteVideo.uid = uid + } else { + LogUtils.log(message: "no video canvas available for \(uid), cancel bind", level: .warning) + } + setFirstUserPlaybackVolumeSliderEnable() + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + if let remoteVideo = videos.first(where: { $0.uid == uid }) { + remoteVideo.uid = nil + } else { + LogUtils.log(message: "no matching video canvas for \(uid), cancel unbind", level: .warning) + } + setFirstUserPlaybackVolumeSliderEnable() + } + + /// Reports the statistics of the current call. The SDK triggers this callback once every two seconds after the user joins the channel. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, reportRtcStats stats: AgoraChannelStats) { + videos[0].statsInfo?.updateChannelStats(stats) + } + + /// Reports the statistics of the uploading local audio streams once every two seconds. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, localAudioStats stats: AgoraRtcLocalAudioStats) { + videos[0].statsInfo?.updateLocalAudioStats(stats) + } + + /// Reports the statistics of the audio stream from each remote user/host. + /// @param stats stats struct for current call statistics + func rtcEngine(_ engine: AgoraRtcEngineKit, remoteAudioStats stats: AgoraRtcRemoteAudioStats) { + videos.first(where: { $0.uid == stats.uid })?.statsInfo?.updateAudioStats(stats) + } + + /// Occurs when the most active speaker is detected. + /// @param speakerUid The user ID of the most active speaker + func rtcEngine(_ engine: AgoraRtcEngineKit, activeSpeaker speakerUid: UInt) { + DispatchQueue.main.async { + self.activeSpeaker.stringValue = (speakerUid as NSNumber).stringValue + } + } + + /// Reports which users are speaking, the speakers' volumes, and whether the local user is speaking. + /// @params speakers volume info for all speakers + /// @params totalVolume Total volume after audio mixing. The value range is [0,255]. + func rtcEngine(_ engine: AgoraRtcEngineKit, reportAudioVolumeIndicationOfSpeakers speakers: [AgoraRtcAudioVolumeInfo], totalVolume: Int) { + for volumeInfo in speakers { + if (volumeInfo.uid == 0) { + videos[0].statsInfo?.updateVolume(volumeInfo.volume) + DispatchQueue.main.async { + self.localUserSpeaking.stringValue = volumeInfo.vad == 1 ? "YES" : "NO" + } + } else { + videos.first(where: { $0.uid == volumeInfo.uid })?.statsInfo?.updateVolume(volumeInfo.volume) + } + } + } +} diff --git a/macOS/APIExample/Examples/Basic/JoinChannelAudio/zh-Hans.lproj/JoinChannelAudio.strings b/macOS/APIExample/Examples/Basic/JoinChannelAudio/zh-Hans.lproj/JoinChannelAudio.strings new file mode 100644 index 000000000..96e4f20bb --- /dev/null +++ b/macOS/APIExample/Examples/Basic/JoinChannelAudio/zh-Hans.lproj/JoinChannelAudio.strings @@ -0,0 +1,21 @@ + +/* Class = "NSTextFieldCell"; title = "NO"; ObjectID = "3mR-iP-I85"; */ +"3mR-iP-I85.title" = "NO"; + +/* Class = "NSTextFieldCell"; title = "Local user speaking status:"; ObjectID = "6jk-Ev-4bY"; */ +"6jk-Ev-4bY.title" = "是否说话:"; + +/* Class = "NSButtonCell"; title = "Join"; ObjectID = "B4L-Fw-EuA"; */ +"B4L-Fw-EuA.title" = "Join"; + +/* Class = "NSTextFieldCell"; title = "NO"; ObjectID = "QrQ-dw-wre"; */ +"QrQ-dw-wre.title" = "NO"; + +/* Class = "NSTextFieldCell"; title = "Active Remote Speaker:"; ObjectID = "hda-m2-IVQ"; */ +"hda-m2-IVQ.title" = "活跃用户:"; + +/* Class = "NSBox"; title = "Box"; ObjectID = "j41-op-nLI"; */ +"j41-op-nLI.title" = "Box"; + +/* Class = "NSViewController"; title = "Join Channel Audio"; ObjectID = "jAv-ZA-ecf"; */ +"jAv-ZA-ecf.title" = "Join Channel Audio"; diff --git a/macOS/APIExample/Examples/Basic/JoinChannelVideo/Base.lproj/JoinChannelVideo.storyboard b/macOS/APIExample/Examples/Basic/JoinChannelVideo/Base.lproj/JoinChannelVideo.storyboard new file mode 100644 index 000000000..8372d1ea6 --- /dev/null +++ b/macOS/APIExample/Examples/Basic/JoinChannelVideo/Base.lproj/JoinChannelVideo.storyboard @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macOS/APIExample/Examples/Basic/JoinChannelVideo/JoinChannelVideo.swift b/macOS/APIExample/Examples/Basic/JoinChannelVideo/JoinChannelVideo.swift new file mode 100644 index 000000000..d25bfa019 --- /dev/null +++ b/macOS/APIExample/Examples/Basic/JoinChannelVideo/JoinChannelVideo.swift @@ -0,0 +1,483 @@ +// +// JoinChannelVC.swift +// APIExample +// +// Created by 张乾泽 on 2020/4/17. +// Copyright © 2020 Agora Corp. All rights reserved. +// +import Cocoa +import AgoraRtcKit +import AGEVideoLayout + +class JoinChannelVideoMain: BaseViewController { + + var agoraKit: AgoraRtcEngineKit! + + var videos: [VideoView] = [] + @IBOutlet weak var Container: AGEVideoContainer! + + /** + --- Cameras Picker --- + */ + @IBOutlet weak var selectCameraPicker: Picker! + var cameras: [AgoraRtcDeviceInfo] = [] { + didSet { + DispatchQueue.main.async {[unowned self] in + self.selectCameraPicker.picker.addItems(withTitles: self.cameras.map {$0.deviceName ?? "unknown"}) + } + } + } + var selectedCamera: AgoraRtcDeviceInfo? { + let index = selectCameraPicker.indexOfSelectedItem + if index >= 0 && index < cameras.count { + return cameras[index] + } else { + return nil + } + } + func initSelectCameraPicker() { + selectCameraPicker.label.stringValue = "Camera".localized + // find device in a separate thread to avoid blocking main thread + let queue = DispatchQueue(label: "device.enumerateDevices") + queue.async {[unowned self] in + self.cameras = self.agoraKit.enumerateDevices(.videoCapture) ?? [] + } + + selectCameraPicker.onSelectChanged { + if !self.isJoined { + return + } + // use selected devices + guard let cameraId = self.selectedCamera?.deviceId else { + return + } + self.agoraKit.setDevice(.videoCapture, deviceId: cameraId) + } + } + + /** + --- Resolutions Picker --- + */ + @IBOutlet weak var selectResolutionPicker: Picker! + var selectedResolution: Resolution? { + let index = self.selectResolutionPicker.indexOfSelectedItem + if index >= 0 && index < Configs.Resolutions.count { + return Configs.Resolutions[index] + } else { + return nil + } + } + func initSelectResolutionPicker() { + selectResolutionPicker.label.stringValue = "Resolution".localized + selectResolutionPicker.picker.addItems(withTitles: Configs.Resolutions.map { $0.name() }) + selectResolutionPicker.picker.selectItem(at: GlobalSettings.shared.resolutionSetting.selectedOption().value) + + selectResolutionPicker.onSelectChanged { + if !self.isJoined { + return + } + + guard let resolution = self.selectedResolution, + let fps = self.selectedFps else { + return + } + self.agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + } + } + + /** + --- Fps Picker --- + */ + @IBOutlet weak var selectFpsPicker: Picker! + var selectedFps: Int? { + let index = self.selectFpsPicker.indexOfSelectedItem + if index >= 0 && index < Configs.Fps.count { + return Configs.Fps[index] + } else { + return nil + } + } + func initSelectFpsPicker() { + selectFpsPicker.label.stringValue = "Frame Rate".localized + selectFpsPicker.picker.addItems(withTitles: Configs.Fps.map { "\($0)fps" }) + selectFpsPicker.picker.selectItem(at: GlobalSettings.shared.fpsSetting.selectedOption().value) + + selectFpsPicker.onSelectChanged { + if !self.isJoined { + return + } + + guard let resolution = self.selectedResolution, + let fps = self.selectedFps else { + return + } + self.agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + } + } + + /** + --- Microphones Picker --- + */ + @IBOutlet weak var selectMicsPicker: Picker! + var mics: [AgoraRtcDeviceInfo] = [] { + didSet { + DispatchQueue.main.async {[unowned self] in + self.selectMicsPicker.picker.addItems(withTitles: self.mics.map {$0.deviceName ?? "unknown"}) + } + } + } + var selectedMicrophone: AgoraRtcDeviceInfo? { + let index = self.selectMicsPicker.indexOfSelectedItem + if index >= 0 && index < mics.count { + return mics[index] + } else { + return nil + } + } + func initSelectMicsPicker() { + selectMicsPicker.label.stringValue = "Microphone".localized + // find device in a separate thread to avoid blocking main thread + let queue = DispatchQueue(label: "device.enumerateDevices") + queue.async {[unowned self] in + self.mics = self.agoraKit.enumerateDevices(.audioRecording) ?? [] + } + + selectMicsPicker.onSelectChanged { + if !self.isJoined { + return + } + // use selected devices + guard let micId = self.selectedMicrophone?.deviceId else { + return + } + self.agoraKit.setDevice(.audioRecording, deviceId: micId) + } + } + + /** + --- Layout Picker --- + */ + @IBOutlet weak var selectLayoutPicker: Picker! + let layouts = [Layout("1v1", 2), Layout("1v3", 4), Layout("1v8", 9), Layout("1v15", 16)] + var selectedLayout: Layout? { + let index = self.selectLayoutPicker.indexOfSelectedItem + if index >= 0 && index < layouts.count { + return layouts[index] + } else { + return nil + } + } + func initSelectLayoutPicker() { + layoutVideos(2) + selectLayoutPicker.label.stringValue = "Layout".localized + selectLayoutPicker.picker.addItems(withTitles: layouts.map { $0.label }) + selectLayoutPicker.onSelectChanged { + if self.isJoined { + return + } + guard let layout = self.selectedLayout else { return } + self.layoutVideos(layout.value) + } + } + + /** + --- Role Picker --- + */ + @IBOutlet weak var selectRolePicker: Picker! + private let roles = AgoraClientRole.allValues() + var selectedRole: AgoraClientRole? { + let index = self.selectRolePicker.indexOfSelectedItem + if index >= 0 && index < roles.count { + return roles[index] + } else { + return nil + } + } + func initSelectRolePicker() { + selectRolePicker.label.stringValue = "Role".localized + selectRolePicker.picker.addItems(withTitles: roles.map { $0.description() }) + selectRolePicker.onSelectChanged { + guard let selected = self.selectedRole else { return } + if self.isJoined { + self.agoraKit.setClientRole(selected) + } + } + } + + /** + --- Channel TextField --- + */ + @IBOutlet weak var channelField: Input! + func initChannelField() { + channelField.label.stringValue = "Channel".localized + channelField.field.placeholderString = "Channel Name".localized + } + + /** + --- Button --- + */ + @IBOutlet weak var joinChannelButton: NSButton! + func initJoinChannelButton() { + joinChannelButton.title = isJoined ? "Leave Channel".localized : "Join Channel".localized + } + + // indicate if current instance has joined channel + var isJoined: Bool = false { + didSet { + channelField.isEnabled = !isJoined + selectLayoutPicker.isEnabled = !isJoined + initJoinChannelButton() + } + } + + // indicate for doing something + var isProcessing: Bool = false { + didSet { + joinChannelButton.isEnabled = !isProcessing + } + } + + override func viewDidLoad() { + super.viewDidLoad() + // Do view setup here. + let config = AgoraRtcEngineConfig() + config.appId = KeyCenter.AppId + config.areaCode = GlobalSettings.shared.area.rawValue + agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self) + agoraKit.enableVideo() + + initSelectCameraPicker() + initSelectResolutionPicker() + initSelectFpsPicker() + initSelectMicsPicker() + initSelectLayoutPicker() + initSelectRolePicker() + initChannelField() + initJoinChannelButton() + } + + func layoutVideos(_ count: Int) { + videos = [] + for i in 0...count - 1 { + let view = VideoView.createFromNib()! + if(i == 0) { + view.placeholder.stringValue = "Local" + view.type = .local + view.statsInfo = StatisticsInfo(type: .local(StatisticsInfo.LocalInfo())) + } else { + view.placeholder.stringValue = "Remote \(i)" + view.type = .remote + view.statsInfo = StatisticsInfo(type: .remote(StatisticsInfo.RemoteInfo())) + } + videos.append(view) + } + // layout render view + Container.layoutStream(views: videos) + } + + @IBAction func onVideoCallButtonPressed(_ sender: NSButton) { + if !isJoined { + // check configuration + let channel = channelField.stringValue + if channel.isEmpty { + return + } + guard let cameraId = selectedCamera?.deviceId, + let resolution = selectedResolution, + let micId = selectedMicrophone?.deviceId, + let role = selectedRole, + let fps = selectedFps else { + return + } + + agoraKit.setDevice(.videoCapture, deviceId: cameraId) + agoraKit.setDevice(.audioRecording, deviceId: micId) + // set live broadcaster mode + agoraKit.setChannelProfile(.liveBroadcasting) + // set myself as broadcaster to stream video/audio + agoraKit.setClientRole(role) + agoraKit.setVideoEncoderConfiguration( + AgoraVideoEncoderConfiguration( + size: resolution.size(), + frameRate: AgoraVideoFrameRate(rawValue: fps) ?? .fps15, + bitrate: AgoraVideoBitrateStandard, + orientationMode: .adaptative + ) + ) + + // set up local video to render your local camera preview + let localVideo = videos[0] + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = 0 + // the view to be binded + videoCanvas.view = localVideo.videocanvas + videoCanvas.renderMode = .hidden + agoraKit.setupLocalVideo(videoCanvas) + + // start joining channel + // 1. Users can only see each other after they join the + // same channel successfully using the same app id. + // 2. If app certificate is turned on at dashboard, token is needed + // when joining channel. The channel name and uid used to calculate + // the token has to match the ones used for channel join + isProcessing = true + let option = AgoraRtcChannelMediaOptions() + let result = agoraKit.joinChannel(byToken: KeyCenter.Token, channelId: channel, info: nil, uid: 0, options: option) + if result != 0 { + isProcessing = false + // Usually happens with invalid parameters + // Error code description can be found at: + // en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + // cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + self.showAlert(title: "Error", message: "joinChannel call failed: \(result), please check your params") + } + } else { + isProcessing = true + agoraKit.leaveChannel { (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + self.isProcessing = false + self.videos[0].uid = nil + self.isJoined = false + self.videos.forEach { + $0.uid = nil + $0.statsLabel.stringValue = "" + } + } + } + } + + override func viewWillBeRemovedFromSplitView() { + if isJoined { + agoraKit.disableVideo() + agoraKit.leaveChannel { (stats:AgoraChannelStats) in + LogUtils.log(message: "Left channel", level: .info) + } + } + } +} + +/// agora rtc engine delegate events +extension JoinChannelVideoMain: AgoraRtcEngineDelegate { + /// callback when warning occured for agora sdk, warning can usually be ignored, still it's nice to check out + /// what is happening + /// Warning code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraWarningCode.html + /// @param warningCode warning code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurWarning warningCode: AgoraWarningCode) { + LogUtils.log(message: "warning: \(warningCode.rawValue)", level: .warning) + } + + /// callback when error occured for agora sdk, you are recommended to display the error descriptions on demand + /// to let user know something wrong is happening + /// Error code description can be found at: + /// en: https://docs.agora.io/en/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// cn: https://docs.agora.io/cn/Voice/API%20Reference/oc/Constants/AgoraErrorCode.html + /// @param errorCode error code of the problem + func rtcEngine(_ engine: AgoraRtcEngineKit, didOccurError errorCode: AgoraErrorCode) { + LogUtils.log(message: "error: \(errorCode)", level: .error) + if self.isProcessing { + self.isProcessing = false + } + self.showAlert(title: "Error", message: "Error \(errorCode.rawValue) occur") + } + + /// callback when the local user joins a specified channel. + /// @param channel + /// @param uid uid of local user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinChannel channel: String, withUid uid: UInt, elapsed: Int) { + isProcessing = false + isJoined = true + let localVideo = videos[0] + localVideo.uid = uid + LogUtils.log(message: "Join \(channel) with uid \(uid) elapsed \(elapsed)ms", level: .info) + } + + /// callback when a remote user is joinning the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param elapsed time elapse since current sdk instance join the channel in ms + func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) { + LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info) + + // find a VideoView w/o uid assigned + if let remoteVideo = videos.first(where: { $0.uid == nil }) { + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = remoteVideo.videocanvas + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + remoteVideo.uid = uid + } else { + LogUtils.log(message: "no video canvas available for \(uid), cancel bind", level: .warning) + } + } + + /// callback when a remote user is leaving the channel, note audience in live broadcast mode will NOT trigger this event + /// @param uid uid of remote joined user + /// @param reason reason why this user left, note this event may be triggered when the remote user + /// become an audience in live broadcasting profile + func rtcEngine(_ engine: AgoraRtcEngineKit, didOfflineOfUid uid: UInt, reason: AgoraUserOfflineReason) { + LogUtils.log(message: "remote user left: \(uid) reason \(reason)", level: .info) + + // to unlink your view from sdk, so that your view reference will be released + // note the video will stay at its last frame, to completely remove it + // you will need to remove the EAGL sublayer from your binded view + if let remoteVideo = videos.first(where: { $0.uid == uid }) { + let videoCanvas = AgoraRtcVideoCanvas() + videoCanvas.uid = uid + // the view to be binded + videoCanvas.view = nil + videoCanvas.renderMode = .hidden + agoraKit.setupRemoteVideo(videoCanvas) + remoteVideo.uid = nil + } else { + LogUtils.log(message: "no matching video canvas for \(uid), cancel unbind", level: .warning) + } + } + + /// Reports the statistics of the current call. The SDK triggers this callback once every two seconds after the user joins the channel. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, reportRtcStats stats: AgoraChannelStats) { + videos[0].statsInfo?.updateChannelStats(stats) + } + + /// Reports the statistics of the uploading local video streams once every two seconds. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, localVideoStats stats: AgoraRtcLocalVideoStats) { + videos[0].statsInfo?.updateLocalVideoStats(stats) + } + + /// Reports the statistics of the uploading local audio streams once every two seconds. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, localAudioStats stats: AgoraRtcLocalAudioStats) { + videos[0].statsInfo?.updateLocalAudioStats(stats) + } + + /// Reports the statistics of the video stream from each remote user/host. + /// @param stats stats struct + func rtcEngine(_ engine: AgoraRtcEngineKit, remoteVideoStats stats: AgoraRtcRemoteVideoStats) { + videos.first(where: { $0.uid == stats.uid })?.statsInfo?.updateVideoStats(stats) + } + + /// Reports the statistics of the audio stream from each remote user/host. + /// @param stats stats struct for current call statistics + func rtcEngine(_ engine: AgoraRtcEngineKit, remoteAudioStats stats: AgoraRtcRemoteAudioStats) { + videos.first(where: { $0.uid == stats.uid })?.statsInfo?.updateAudioStats(stats) + } +} diff --git a/macOS/APIExample/Examples/Basic/JoinChannelVideo/zh-Hans.lproj/JoinChannelVideo.strings b/macOS/APIExample/Examples/Basic/JoinChannelVideo/zh-Hans.lproj/JoinChannelVideo.strings new file mode 100644 index 000000000..8f923c89e --- /dev/null +++ b/macOS/APIExample/Examples/Basic/JoinChannelVideo/zh-Hans.lproj/JoinChannelVideo.strings @@ -0,0 +1,24 @@ + +/* Class = "NSButtonCell"; title = "Leave"; ObjectID = "4rc-r1-Ay6"; */ +"4rc-r1-Ay6.title" = "离开频道"; + +/* Class = "NSMenuItem"; title = "1V1"; ObjectID = "Iws-j3-l2h"; */ +"Iws-j3-l2h.title" = "1V1"; + +/* Class = "NSMenuItem"; title = "1V15"; ObjectID = "Mmi-d8-vOm"; */ +"Mmi-d8-vOm.title" = "1V15"; + +/* Class = "NSTextFieldCell"; placeholderString = "加入频道"; ObjectID = "PtD-n2-sEW"; */ +"PtD-n2-sEW.placeholderString" = "输入频道号"; + +/* Class = "NSMenuItem"; title = "1V3"; ObjectID = "VNU-so-ajb"; */ +"VNU-so-ajb.title" = "1V3"; + +/* Class = "NSViewController"; title = "Join Channel Video"; ObjectID = "YjT-yy-DnJ"; */ +"YjT-yy-DnJ.title" = "实时视频通话/直播"; + +/* Class = "NSMenuItem"; title = "1V8"; ObjectID = "cH4-ft-u77"; */ +"cH4-ft-u77.title" = "1V8"; + +/* Class = "NSButtonCell"; title = "Join"; ObjectID = "guU-jX-Wkg"; */ +"guU-jX-Wkg.title" = "加入频道"; diff --git a/macOS/APIExample/Info.plist b/macOS/APIExample/Info.plist new file mode 100644 index 000000000..86f42590e --- /dev/null +++ b/macOS/APIExample/Info.plist @@ -0,0 +1,40 @@ + + + + + NSMicrophoneUsageDescription + + NSCameraUsageDescription + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + Copyright © 2020 Agora Corp. All rights reserved. + NSMainStoryboardFile + Main + NSPrincipalClass + NSApplication + NSSupportsAutomaticTermination + + NSSupportsSuddenTermination + + + diff --git a/macOS/APIExample/Localizable.strings b/macOS/APIExample/Localizable.strings new file mode 100644 index 000000000..c4f6e9167 --- /dev/null +++ b/macOS/APIExample/Localizable.strings @@ -0,0 +1,155 @@ +/* + Localization.strings + APIExample + + Created by 张乾泽 on 2020/10/7. + Copyright © 2020 Agora Corp. All rights reserved. +*/ + +"Join a channel (Video)" = "实时视频通话/直播"; +"Join a channel (Audio)" = "实时语音通话/直播"; +"RTMP Streaming" = "RTMP旁路推流"; +"Media Injection" = "流媒体注入"; +"Video Metadata" = "SEI消息"; +"Voice Changer" = "美声/音效"; +"Custom Audio Source" = "音频自采集"; +"Custom Audio Render" = "音频自渲染"; +"Custom Video Source(MediaIO)" = "视频自采集(MediaIO)"; +"Custom Video Source(Push)" = "视频自采集(Push)"; +"Custom Video Render" = "视频自渲染(Metal)"; +"Quick Switch Channel" = "快速切换频道"; +"Join Multiple Channels" = "加入多频道"; +"Stream Encryption" = "音视频流加密"; +"Audio Mixing" = "音频文件混音"; +"Raw Media Data" = "音视频裸数据"; +"Precall Test" = "通话前网络/设备测试"; +"Media Player" = "流媒体播放器"; +"Screen Share" = "屏幕共享"; +"Super Resolution" = "超级分辨率"; +"Media Channel Relay" = "跨频道流转发"; +"Set Resolution" = "设置视频分辨率"; +"Set Fps" = "设置视频帧率"; +"Set Orientation" = "设置视频朝向"; +"Set Chat Beautifier" = "设置语聊美声"; +"Set Timbre Transformation" = "设置音色变换"; +"Set Voice Changer" = "设置变声音效"; +"Set Style Transformation" = "设置曲风音效"; +"Set Room Acoustics" = "设置空间音效"; +"Set Band Frequency" = "设置波段频率"; +"Set Reverb Key" = "设置混响属性"; +"Set Encryption Mode" = "设置加密模式"; +"fixed portrait" = "固定纵向"; +"fixed landscape" = "固定横向"; +"adaptive" = "自适应"; +"Local Host" = "本地预览"; +"Remote Host" = "远端视频"; +"Set Audio Profile" = "设置音频参数配置"; +"Set Audio Scenario" = "设置音频使用场景"; +"Default" = "默认"; +"Music Standard" = "标准音乐"; +"Music Standard Stereo" = "标准双声道音乐"; +"Music High Quality" = "高音质音乐"; +"Music High Quality Stereo" = "高音质双声道音乐"; +"Speech Standard" = "标准人声"; +"Chat Room Gaming" = "娱乐语聊房"; +"Education" = "教育"; +"Game Streaming" = "高音质语聊房"; +"Chat Room Entertainment" = "游戏开黑"; +"Show Room" = "秀场"; +"Cancel" = "取消"; +"Off" = "原声"; +"FemaleFresh" = "语聊美声: 清新(女)"; +"FemaleVitality" = "语聊美声: 活力(女)"; +"MaleMagnetic" = "语聊美声: 磁性(男)"; +"Vigorous" = "浑厚"; +"Deep" = "低沉"; +"Mellow" = "圆润"; +"Falsetto" = "假音"; +"Full" = "饱满"; +"Clear" = "清澈"; +"Resounding" = "高亢"; +"Ringing" = "嘹亮"; +"Spacial" = "空旷"; +"Ethereal" = "空灵"; +"Old Man" = "老男孩"; +"Baby Boy" = "小男孩"; +"Baby Girl" = "小女孩"; +"ZhuBaJie" = "猪八戒"; +"Hulk" = "绿巨人"; +"FxUncle" = "大叔"; +"FxSister" = "小姐姐"; +"Pop" = "流行"; +"Pop(Old Version)" = "流行(旧版)"; +"R&B" = "R&B"; +"R&B(Old Version)" = "R&B(旧版)"; +"Rock" = "摇滚"; +"HipHop" = "嘻哈"; +"Vocal Concert" = "演唱会"; +"Vocal Concert(Old Version)" = "演唱会(旧版)"; +"KTV" = "KTV"; +"KTV(Old Version)" = "KTV(旧版)"; +"Studio" = "录音棚"; +"Studio(Old Version)" = "录音棚(旧版)"; +"Phonograph" = "留声机"; +"Virtual Stereo" = "虚拟立体声"; +"Dry Level" = "原始声音强度"; +"Wet Level" = "早期反射信号强度"; +"Room Size" = "房间尺寸"; +"Wet Delay" = "早期反射信号延迟"; +"Strength" = "混响持续强度"; +"Broadcaster" = "主播"; +"Audience" = "观众"; +"Global settings" = "全局设置"; + +"Resolution" = "分辨率"; +"Frame Rate" = "帧率"; +"Camera" = "摄像头"; +"Microphone" = "麦克风"; +"Layout" = "布局"; +"Role" = "角色"; +"Channel" = "频道号"; +"Channel Name" = "输入频道号"; +"Join Channel" = "加入频道"; +"Leave Channel" = "离开频道"; +"Audio Profile" = "音频音质参数"; +"Audio Scenario" = "音频使用场景"; +"Device Recording Volume" = "设备录制音量"; +"SDK Recording Volume" = "SDK录制音量"; +"Device Playout Volume" = "设备播放音量"; +"SDK Playout Volume" = "SDK播放音量"; +"User Playback Volume" = "首位远端用户音量"; +"Encryption Mode" = "加密模式"; +"Encryption Secret" = "加密密码"; +"Input Encryption Secret" = "输入加密密码"; +"Relay Channel" = "转发频道"; +"Start Relay" = "开始转发"; +"Relay Channnel Name" = "目标转发频道名"; +"Stop Relay" = "停止转发"; +"Display Share" = "屏幕共享"; +"Window Share" = "窗口共享"; +"Stop Share" = "停止共享"; +"Share Half Screen" = "分享部分区域"; +"Publish" = "发流"; +"Unpublish" = "停止发流"; +"Mixing Volume" = "混音音量"; +"Mixing Playback Volume" = "混音播放音量"; +"Mixing Publish Volume" = "混音发布音量"; +"Overall Effect Volume" = "音效音量"; +"Chat Beautifier" = "语聊美声"; +"Timbre Transformation" = "音色转换"; +"Style Transformation" = "风格转换"; +"Room Acoustics" = "室内声学"; +"Pitch Correction" = "音高修正"; +"Cycle(0-60)" = "循环周期(0-60)秒"; +"Tonic Mode(1-3)" = "主音模式(1-3)"; +"Tonic Pitch(1-12)" = "主音音高(1-12)"; +"Voice Pitch" = "声调"; +"Off" = "关闭"; +"Set Audio Effect Params" = "设置参数"; +"Equalization Band" = "波段增益"; +"Create Data Stream" = "创建数据流"; +"Send Message" = "发送消息"; +"Input Message" = "输入消息"; +"Send" = "发送"; +"Sending" = "发送中"; +"Raw Audio Data" = "音频裸数据"; diff --git a/macOS/APIExample/Resources/audioeffect.mp3 b/macOS/APIExample/Resources/audioeffect.mp3 new file mode 100644 index 000000000..edde60d5c Binary files /dev/null and b/macOS/APIExample/Resources/audioeffect.mp3 differ diff --git a/macOS/APIExample/Resources/audiomixing.mp3 b/macOS/APIExample/Resources/audiomixing.mp3 new file mode 100644 index 000000000..0379b4d74 Binary files /dev/null and b/macOS/APIExample/Resources/audiomixing.mp3 differ diff --git a/macOS/APIExample/Resources/effectA.wav b/macOS/APIExample/Resources/effectA.wav new file mode 100644 index 000000000..dc31fdb68 Binary files /dev/null and b/macOS/APIExample/Resources/effectA.wav differ diff --git a/macOS/APIExample/SettingsController.swift b/macOS/APIExample/SettingsController.swift new file mode 100644 index 000000000..97d13a890 --- /dev/null +++ b/macOS/APIExample/SettingsController.swift @@ -0,0 +1,44 @@ +// +// SettingsController.swift +// APIExample +// +// Created by XC on 2020/12/15. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import Cocoa + +class SettingsController: BaseViewController { + + @IBOutlet weak var resolutionLabel: NSTextField! + @IBOutlet weak var resolutionPicker: NSPopUpButton! + + @IBOutlet weak var fpsLabel: NSTextField! + @IBOutlet weak var fpsPicker: NSPopUpButton! + + @IBOutlet weak var sdkVersionLabel: NSTextField! + @IBOutlet weak var sdkVersion: NSTextField! + + override func viewDidLoad() { + super.viewDidLoad() + // Do view setup here. + self.resolutionLabel.cell?.title = "Resolution".localized + self.resolutionPicker.addItems(withTitles: GlobalSettings.shared.resolutionSetting.options.map { $0.label }) + self.resolutionPicker.selectItem(at: GlobalSettings.shared.resolutionSetting.selected) + + self.fpsLabel.cell?.title = "Frame Rate".localized + self.fpsPicker.addItems(withTitles: GlobalSettings.shared.fpsSetting.options.map { $0.label }) + self.fpsPicker.selectItem(at: GlobalSettings.shared.fpsSetting.selected) + + self.sdkVersion.cell?.title = "v\(AgoraRtcEngineKit.getSdkVersion())" + } + + @IBAction func onResolutionChanged(_ sender: NSPopUpButton) { + GlobalSettings.shared.resolutionSetting.selected = sender.indexOfSelectedItem + } + + @IBAction func onFpsChanged(_ sender: NSPopUpButton) { + GlobalSettings.shared.fpsSetting.selected = sender.indexOfSelectedItem + } +} + diff --git a/macOS/APIExample/ViewController.swift b/macOS/APIExample/ViewController.swift new file mode 100644 index 000000000..db2a7adf6 --- /dev/null +++ b/macOS/APIExample/ViewController.swift @@ -0,0 +1,131 @@ +// +// ViewController.swift +// APIExample +// +// Created by 张乾泽 on 2020/8/28. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import Cocoa + +struct MenuItem { + var name: String + var identifier: String + var controller: String? + var storyboard: String? +} + +class MenuController: NSViewController { + + let settings = MenuItem(name: "Global settings".localized, identifier: "menuCell", controller: "Settings", storyboard: "Settings") + + var menus:[MenuItem] = [ + MenuItem(name: "Basic", identifier: "headerCell"), + MenuItem(name: "Join a channel (Video)".localized, identifier: "menuCell", controller: "JoinChannelVideo", storyboard: "JoinChannelVideo"), + MenuItem(name: "Join a channel (Audio)".localized, identifier: "menuCell", controller: "JoinChannelAudio", storyboard: "JoinChannelAudio"), + MenuItem(name: "Anvanced", identifier: "headerCell"), + MenuItem(name: "RTMP Streaming".localized, identifier: "menuCell", controller: "RTMPStreaming", storyboard: "RTMPStreaming"), + MenuItem(name: "Custom Video Source(MediaIO)".localized, identifier: "menuCell", controller: "CustomVideoSourceMediaIO", storyboard: "CustomVideoSourceMediaIO"), + MenuItem(name: "Custom Video Source(Push)".localized, identifier: "menuCell", controller: "CustomVideoSourcePush", storyboard: "CustomVideoSourcePush"), + MenuItem(name: "Custom Video Render".localized, identifier: "menuCell", controller: "CustomVideoRender", storyboard: "CustomVideoRender"), + MenuItem(name: "Custom Audio Source".localized, identifier: "menuCell", controller: "CustomAudioSource", storyboard: "CustomAudioSource"), + MenuItem(name: "Custom Audio Render".localized, identifier: "menuCell", controller: "CustomAudioRender", storyboard: "CustomAudioRender"), + MenuItem(name: "Raw Media Data".localized, identifier: "menuCell", controller: "RawMediaData", storyboard: "RawMediaData"), + MenuItem(name: "Join Multiple Channels".localized, identifier: "menuCell", controller: "JoinMultipleChannel", storyboard: "JoinMultiChannel"), + MenuItem(name: "Stream Encryption".localized, identifier: "menuCell", controller: "StreamEncryption", storyboard: "StreamEncryption"), + MenuItem(name: "Screen Share".localized, identifier: "menuCell", controller: "ScreenShare", storyboard: "ScreenShare"), + MenuItem(name: "Media Channel Relay".localized, identifier: "menuCell", controller: "ChannelMediaRelay", storyboard: "ChannelMediaRelay"), + MenuItem(name: "Audio Mixing".localized, identifier: "menuCell", controller: "AudioMixing", storyboard: "AudioMixing"), + MenuItem(name: "Voice Changer".localized, identifier: "menuCell", controller: "VoiceChanger", storyboard: "VoiceChanger"), + MenuItem(name: "Precall Test".localized, identifier: "menuCell", controller: "PrecallTest", storyboard: "PrecallTest"), + MenuItem(name: "Create Data Stream".localized, identifier: "menuCell", controller: "CreateDataStream", storyboard: "CreateDataStream"), + MenuItem(name: "Raw Audio Data".localized, identifier: "menuCell", controller: "RawAudioData", storyboard: "RawAudioData") + ] + @IBOutlet weak var tableView:NSTableView! + + override func viewDidLoad() { + super.viewDidLoad() + } + + @IBAction func onClickSetting(_ sender: NSButton) { + let selectedRow = tableView.selectedRow + if (selectedRow >= 0) { + tableView.deselectRow(selectedRow) + } + loadSplitViewItem(item: settings) + } + + func loadSplitViewItem(item: MenuItem) { + var storyboardName = "" + + if let name = item.storyboard { + storyboardName = name + } else { + storyboardName = "Main" + } + let board: NSStoryboard = NSStoryboard(name: storyboardName, bundle: nil) + + guard let splitViewController = self.parent as? NSSplitViewController, + let controllerIdentifier = item.controller, + let viewController = board.instantiateController(withIdentifier: controllerIdentifier) as? BaseView else { return } + + let splititem = NSSplitViewItem(viewController: viewController as NSViewController) + + let detailItem = splitViewController.splitViewItems[1] + if let detailViewController = detailItem.viewController as? BaseView { + detailViewController.viewWillBeRemovedFromSplitView() + } + splitViewController.removeSplitViewItem(detailItem) + splitViewController.addSplitViewItem(splititem) + } +} + +extension MenuController: NSTableViewDataSource, NSTableViewDelegate { + func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { + let item = menus[row] + return item.identifier == "menuCell" ? 32 : 18 + } + + func numberOfRows(in tableView: NSTableView) -> Int { + return menus.count + } + + func tableView(_ tableView: NSTableView, shouldSelectRow row: Int) -> Bool { + let item = menus[row] + return item.identifier != "headerCell" + } + + func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + let item = menus[row] + // Get an existing cell with the MyView identifier if it exists + let view = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: item.identifier), owner: self) as? NSTableCellView + + view?.imageView?.image = nil + view?.textField?.stringValue = item.name + + // Return the result + return view; + } + + func tableViewSelectionDidChange(_ notification: Notification) { + if (tableView.selectedRow >= 0) { + loadSplitViewItem(item: menus[tableView.selectedRow]) + } + } +} + +class ViewController: NSViewController { + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + } + + override var representedObject: Any? { + didSet { + // Update the view, if already loaded. + } + } +} + diff --git a/macOS/APIExample/zh-Hans.lproj/Main.strings b/macOS/APIExample/zh-Hans.lproj/Main.strings new file mode 100644 index 000000000..3507ae982 --- /dev/null +++ b/macOS/APIExample/zh-Hans.lproj/Main.strings @@ -0,0 +1,405 @@ + +/* Class = "NSMenuItem"; title = "Customize Toolbar…"; ObjectID = "1UK-8n-QPP"; */ +"1UK-8n-QPP.title" = "Customize Toolbar…"; + +/* Class = "NSMenuItem"; title = "APIExample"; ObjectID = "1Xt-HY-uBw"; */ +"1Xt-HY-uBw.title" = "APIExample"; + +/* Class = "NSMenu"; title = "Find"; ObjectID = "1b7-l0-nxx"; */ +"1b7-l0-nxx.title" = "Find"; + +/* Class = "NSMenuItem"; title = "Lower"; ObjectID = "1tx-W0-xDw"; */ +"1tx-W0-xDw.title" = "Lower"; + +/* Class = "NSMenuItem"; title = "Raise"; ObjectID = "2h7-ER-AoG"; */ +"2h7-ER-AoG.title" = "Raise"; + +/* Class = "NSMenuItem"; title = "Transformations"; ObjectID = "2oI-Rn-ZJC"; */ +"2oI-Rn-ZJC.title" = "Transformations"; + +/* Class = "NSMenu"; title = "Spelling"; ObjectID = "3IN-sU-3Bg"; */ +"3IN-sU-3Bg.title" = "Spelling"; + +/* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "3Om-Ey-2VK"; */ +"3Om-Ey-2VK.title" = "Use Default"; + +/* Class = "NSMenu"; title = "Speech"; ObjectID = "3rS-ZA-NoH"; */ +"3rS-ZA-NoH.title" = "Speech"; + +/* Class = "NSMenuItem"; title = "Tighten"; ObjectID = "46P-cB-AYj"; */ +"46P-cB-AYj.title" = "Tighten"; + +/* Class = "NSMenuItem"; title = "Find"; ObjectID = "4EN-yA-p0u"; */ +"4EN-yA-p0u.title" = "Find"; + +/* Class = "NSMenuItem"; title = "Enter Full Screen"; ObjectID = "4J7-dP-txa"; */ +"4J7-dP-txa.title" = "Enter Full Screen"; + +/* Class = "NSMenuItem"; title = "Quit APIExample"; ObjectID = "4sb-4s-VLi"; */ +"4sb-4s-VLi.title" = "Quit APIExample"; + +/* Class = "NSMenuItem"; title = "Edit"; ObjectID = "5QF-Oa-p0T"; */ +"5QF-Oa-p0T.title" = "Edit"; + +/* Class = "NSMenuItem"; title = "Copy Style"; ObjectID = "5Vv-lz-BsD"; */ +"5Vv-lz-BsD.title" = "Copy Style"; + +/* Class = "NSMenuItem"; title = "About APIExample"; ObjectID = "5kV-Vb-QxS"; */ +"5kV-Vb-QxS.title" = "About APIExample"; + +/* Class = "NSMenuItem"; title = "Redo"; ObjectID = "6dh-zS-Vam"; */ +"6dh-zS-Vam.title" = "Redo"; + +/* Class = "NSMenuItem"; title = "Correct Spelling Automatically"; ObjectID = "78Y-hA-62v"; */ +"78Y-hA-62v.title" = "Correct Spelling Automatically"; + +/* Class = "NSMenu"; title = "Writing Direction"; ObjectID = "8mr-sm-Yjd"; */ +"8mr-sm-Yjd.title" = "Writing Direction"; + +/* Class = "NSMenuItem"; title = "Substitutions"; ObjectID = "9ic-FL-obx"; */ +"9ic-FL-obx.title" = "Substitutions"; + +/* Class = "NSMenuItem"; title = "Smart Copy/Paste"; ObjectID = "9yt-4B-nSM"; */ +"9yt-4B-nSM.title" = "Smart Copy/Paste"; + +/* Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; */ +"AYu-sK-qS6.title" = "Main Menu"; + +/* Class = "NSMenuItem"; title = "Preferences…"; ObjectID = "BOF-NM-1cW"; */ +"BOF-NM-1cW.title" = "Preferences…"; + +/* Class = "NSMenuItem"; title = "\tLeft to Right"; ObjectID = "BgM-ve-c93"; */ +"BgM-ve-c93.title" = "\tLeft to Right"; + +/* Class = "NSMenuItem"; title = "Save As…"; ObjectID = "Bw7-FT-i3A"; */ +"Bw7-FT-i3A.title" = "Save As…"; + +/* Class = "NSMenuItem"; title = "Close"; ObjectID = "DVo-aG-piG"; */ +"DVo-aG-piG.title" = "Close"; + +/* Class = "NSMenuItem"; title = "Spelling and Grammar"; ObjectID = "Dv1-io-Yv7"; */ +"Dv1-io-Yv7.title" = "Spelling and Grammar"; + +/* Class = "NSMenu"; title = "Help"; ObjectID = "F2S-fz-NVQ"; */ +"F2S-fz-NVQ.title" = "Help"; + +/* Class = "NSMenuItem"; title = "APIExample Help"; ObjectID = "FKE-Sm-Kum"; */ +"FKE-Sm-Kum.title" = "APIExample Help"; + +/* Class = "NSMenuItem"; title = "Text"; ObjectID = "Fal-I4-PZk"; */ +"Fal-I4-PZk.title" = "Text"; + +/* Class = "NSMenu"; title = "Substitutions"; ObjectID = "FeM-D8-WVr"; */ +"FeM-D8-WVr.title" = "Substitutions"; + +/* Class = "NSMenuItem"; title = "Bold"; ObjectID = "GB9-OM-e27"; */ +"GB9-OM-e27.title" = "Bold"; + +/* Class = "NSMenu"; title = "Format"; ObjectID = "GEO-Iw-cKr"; */ +"GEO-Iw-cKr.title" = "Format"; + +/* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "GUa-eO-cwY"; */ +"GUa-eO-cwY.title" = "Use Default"; + +/* Class = "NSMenuItem"; title = "Font"; ObjectID = "Gi5-1S-RQB"; */ +"Gi5-1S-RQB.title" = "Font"; + +/* Class = "NSMenuItem"; title = "Writing Direction"; ObjectID = "H1b-Si-o9J"; */ +"H1b-Si-o9J.title" = "Writing Direction"; + +/* Class = "NSMenuItem"; title = "View"; ObjectID = "H8h-7b-M4v"; */ +"H8h-7b-M4v.title" = "View"; + +/* Class = "NSMenuItem"; title = "Text Replacement"; ObjectID = "HFQ-gK-NFA"; */ +"HFQ-gK-NFA.title" = "Text Replacement"; + +/* Class = "NSMenuItem"; title = "Show Spelling and Grammar"; ObjectID = "HFo-cy-zxI"; */ +"HFo-cy-zxI.title" = "Show Spelling and Grammar"; + +/* Class = "NSMenu"; title = "View"; ObjectID = "HyV-fh-RgO"; */ +"HyV-fh-RgO.title" = "View"; + +/* Class = "NSMenuItem"; title = "Subscript"; ObjectID = "I0S-gh-46l"; */ +"I0S-gh-46l.title" = "Subscript"; + +/* Class = "NSMenuItem"; title = "Open…"; ObjectID = "IAo-SY-fd9"; */ +"IAo-SY-fd9.title" = "Open…"; + +/* Class = "NSWindow"; title = "Agora API Example"; ObjectID = "IQv-IB-iLA"; */ +"IQv-IB-iLA.title" = "Agora API Example"; + +/* Class = "NSMenuItem"; title = "Justify"; ObjectID = "J5U-5w-g23"; */ +"J5U-5w-g23.title" = "Justify"; + +/* Class = "NSMenuItem"; title = "Use None"; ObjectID = "J7y-lM-qPV"; */ +"J7y-lM-qPV.title" = "Use None"; + +/* Class = "NSMenuItem"; title = "Revert to Saved"; ObjectID = "KaW-ft-85H"; */ +"KaW-ft-85H.title" = "Revert to Saved"; + +/* Class = "NSMenuItem"; title = "Show All"; ObjectID = "Kd2-mp-pUS"; */ +"Kd2-mp-pUS.title" = "Show All"; + +/* Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "LE2-aR-0XJ"; */ +"LE2-aR-0XJ.title" = "Bring All to Front"; + +/* Class = "NSMenuItem"; title = "Paste Ruler"; ObjectID = "LVM-kO-fVI"; */ +"LVM-kO-fVI.title" = "Paste Ruler"; + +/* Class = "NSMenuItem"; title = "\tLeft to Right"; ObjectID = "Lbh-J2-qVU"; */ +"Lbh-J2-qVU.title" = "\tLeft to Right"; + +/* Class = "NSMenuItem"; title = "Copy Ruler"; ObjectID = "MkV-Pr-PK5"; */ +"MkV-Pr-PK5.title" = "Copy Ruler"; + +/* Class = "NSMenuItem"; title = "Services"; ObjectID = "NMo-om-nkz"; */ +"NMo-om-nkz.title" = "Services"; + +/* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "Nlt-pS-UAz"; */ +"Nlt-pS-UAz.title" = "Table View Cell"; + +/* Class = "NSMenuItem"; title = "\tDefault"; ObjectID = "Nop-cj-93Q"; */ +"Nop-cj-93Q.title" = "\tDefault"; + +/* Class = "NSMenuItem"; title = "Minimize"; ObjectID = "OY7-WF-poV"; */ +"OY7-WF-poV.title" = "Minimize"; + +/* Class = "NSMenuItem"; title = "Baseline"; ObjectID = "OaQ-X3-Vso"; */ +"OaQ-X3-Vso.title" = "Baseline"; + +/* Class = "NSMenuItem"; title = "Hide APIExample"; ObjectID = "Olw-nP-bQN"; */ +"Olw-nP-bQN.title" = "Hide APIExample"; + +/* Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "OwM-mh-QMV"; */ +"OwM-mh-QMV.title" = "Find Previous"; + +/* Class = "NSMenuItem"; title = "Stop Speaking"; ObjectID = "Oyz-dy-DGm"; */ +"Oyz-dy-DGm.title" = "Stop Speaking"; + +/* Class = "NSMenuItem"; title = "Bigger"; ObjectID = "Ptp-SP-VEL"; */ +"Ptp-SP-VEL.title" = "Bigger"; + +/* Class = "NSMenuItem"; title = "Show Fonts"; ObjectID = "Q5e-8K-NDq"; */ +"Q5e-8K-NDq.title" = "Show Fonts"; + +/* Class = "NSMenuItem"; title = "Zoom"; ObjectID = "R4o-n2-Eq4"; */ +"R4o-n2-Eq4.title" = "Zoom"; + +/* Class = "NSMenuItem"; title = "\tRight to Left"; ObjectID = "RB4-Sm-HuC"; */ +"RB4-Sm-HuC.title" = "\tRight to Left"; + +/* Class = "NSMenuItem"; title = "Superscript"; ObjectID = "Rqc-34-cIF"; */ +"Rqc-34-cIF.title" = "Superscript"; + +/* Class = "NSMenuItem"; title = "Select All"; ObjectID = "Ruw-6m-B2m"; */ +"Ruw-6m-B2m.title" = "Select All"; + +/* Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "S0p-oC-mLd"; */ +"S0p-oC-mLd.title" = "Jump to Selection"; + +/* Class = "NSMenu"; title = "Window"; ObjectID = "Td7-aD-5lo"; */ +"Td7-aD-5lo.title" = "Window"; + +/* Class = "NSMenuItem"; title = "Capitalize"; ObjectID = "UEZ-Bs-lqG"; */ +"UEZ-Bs-lqG.title" = "Capitalize"; + +/* Class = "NSMenuItem"; title = "Center"; ObjectID = "VIY-Ag-zcb"; */ +"VIY-Ag-zcb.title" = "Center"; + +/* Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO"; */ +"Vdr-fp-XzO.title" = "Hide Others"; + +/* Class = "NSMenuItem"; title = "Italic"; ObjectID = "Vjx-xi-njq"; */ +"Vjx-xi-njq.title" = "Italic"; + +/* Class = "NSMenu"; title = "Edit"; ObjectID = "W48-6f-4Dl"; */ +"W48-6f-4Dl.title" = "Edit"; + +/* Class = "NSMenuItem"; title = "Underline"; ObjectID = "WRG-CD-K1S"; */ +"WRG-CD-K1S.title" = "Underline"; + +/* Class = "NSMenuItem"; title = "New"; ObjectID = "Was-JA-tGl"; */ +"Was-JA-tGl.title" = "New"; + +/* Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "WeT-3V-zwk"; */ +"WeT-3V-zwk.title" = "Paste and Match Style"; + +/* Class = "NSMenuItem"; title = "Find…"; ObjectID = "Xz5-n4-O0W"; */ +"Xz5-n4-O0W.title" = "Find…"; + +/* Class = "NSMenuItem"; title = "Find and Replace…"; ObjectID = "YEy-JH-Tfz"; */ +"YEy-JH-Tfz.title" = "Find and Replace…"; + +/* Class = "NSMenuItem"; title = "\tDefault"; ObjectID = "YGs-j5-SAR"; */ +"YGs-j5-SAR.title" = "\tDefault"; + +/* Class = "NSMenuItem"; title = "Start Speaking"; ObjectID = "Ynk-f8-cLZ"; */ +"Ynk-f8-cLZ.title" = "Start Speaking"; + +/* Class = "NSMenuItem"; title = "Align Left"; ObjectID = "ZM1-6Q-yy1"; */ +"ZM1-6Q-yy1.title" = "Align Left"; + +/* Class = "NSMenuItem"; title = "Paragraph"; ObjectID = "ZvO-Gk-QUH"; */ +"ZvO-Gk-QUH.title" = "Paragraph"; + +/* Class = "NSMenuItem"; title = "Print…"; ObjectID = "aTl-1u-JFS"; */ +"aTl-1u-JFS.title" = "Print…"; + +/* Class = "NSMenuItem"; title = "Window"; ObjectID = "aUF-d1-5bR"; */ +"aUF-d1-5bR.title" = "Window"; + +/* Class = "NSMenu"; title = "Font"; ObjectID = "aXa-aM-Jaq"; */ +"aXa-aM-Jaq.title" = "Font"; + +/* Class = "NSMenuItem"; title = "Use Default"; ObjectID = "agt-UL-0e3"; */ +"agt-UL-0e3.title" = "Use Default"; + +/* Class = "NSMenuItem"; title = "Show Colors"; ObjectID = "bgn-CT-cEk"; */ +"bgn-CT-cEk.title" = "Show Colors"; + +/* Class = "NSMenu"; title = "File"; ObjectID = "bib-Uj-vzu"; */ +"bib-Uj-vzu.title" = "File"; + +/* Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "buJ-ug-pKt"; */ +"buJ-ug-pKt.title" = "Use Selection for Find"; + +/* Class = "NSMenu"; title = "Transformations"; ObjectID = "c8a-y6-VQd"; */ +"c8a-y6-VQd.title" = "Transformations"; + +/* Class = "NSMenuItem"; title = "Use None"; ObjectID = "cDB-IK-hbR"; */ +"cDB-IK-hbR.title" = "Use None"; + +/* Class = "NSMenuItem"; title = "Selection"; ObjectID = "cqv-fj-IhA"; */ +"cqv-fj-IhA.title" = "Selection"; + +/* Class = "NSMenuItem"; title = "Smart Links"; ObjectID = "cwL-P1-jid"; */ +"cwL-P1-jid.title" = "Smart Links"; + +/* Class = "NSMenuItem"; title = "Make Lower Case"; ObjectID = "d9M-CD-aMd"; */ +"d9M-CD-aMd.title" = "Make Lower Case"; + +/* Class = "NSMenu"; title = "Text"; ObjectID = "d9c-me-L2H"; */ +"d9c-me-L2H.title" = "Text"; + +/* Class = "NSMenuItem"; title = "File"; ObjectID = "dMs-cI-mzQ"; */ +"dMs-cI-mzQ.title" = "File"; + +/* Class = "NSMenuItem"; title = "Undo"; ObjectID = "dRJ-4n-Yzg"; */ +"dRJ-4n-Yzg.title" = "Undo"; + +/* Class = "NSMenuItem"; title = "Paste"; ObjectID = "gVA-U4-sdL"; */ +"gVA-U4-sdL.title" = "Paste"; + +/* Class = "NSMenuItem"; title = "Smart Quotes"; ObjectID = "hQb-2v-fYv"; */ +"hQb-2v-fYv.title" = "Smart Quotes"; + +/* Class = "NSMenuItem"; title = "Check Document Now"; ObjectID = "hz2-CU-CR7"; */ +"hz2-CU-CR7.title" = "Check Document Now"; + +/* Class = "NSMenu"; title = "Services"; ObjectID = "hz9-B4-Xy5"; */ +"hz9-B4-Xy5.title" = "Services"; + +/* Class = "NSMenuItem"; title = "Smaller"; ObjectID = "i1d-Er-qST"; */ +"i1d-Er-qST.title" = "Smaller"; + +/* Class = "NSMenu"; title = "Baseline"; ObjectID = "ijk-EB-dga"; */ +"ijk-EB-dga.title" = "Baseline"; + +/* Class = "NSMenuItem"; title = "Kern"; ObjectID = "jBQ-r6-VK2"; */ +"jBQ-r6-VK2.title" = "Kern"; + +/* Class = "NSMenuItem"; title = "\tRight to Left"; ObjectID = "jFq-tB-4Kx"; */ +"jFq-tB-4Kx.title" = "\tRight to Left"; + +/* Class = "NSMenuItem"; title = "Format"; ObjectID = "jxT-CU-nIS"; */ +"jxT-CU-nIS.title" = "Format"; + +/* Class = "NSMenuItem"; title = "Show Sidebar"; ObjectID = "kIP-vf-haE"; */ +"kIP-vf-haE.title" = "Show Sidebar"; + +/* Class = "NSMenuItem"; title = "Check Grammar With Spelling"; ObjectID = "mK6-2p-4JG"; */ +"mK6-2p-4JG.title" = "Check Grammar With Spelling"; + +/* Class = "NSMenuItem"; title = "Ligatures"; ObjectID = "o6e-r0-MWq"; */ +"o6e-r0-MWq.title" = "Ligatures"; + +/* Class = "NSMenu"; title = "Open Recent"; ObjectID = "oas-Oc-fiZ"; */ +"oas-Oc-fiZ.title" = "Open Recent"; + +/* Class = "NSMenuItem"; title = "Loosen"; ObjectID = "ogc-rX-tC1"; */ +"ogc-rX-tC1.title" = "Loosen"; + +/* Class = "NSMenuItem"; title = "Delete"; ObjectID = "pa3-QI-u2k"; */ +"pa3-QI-u2k.title" = "Delete"; + +/* Class = "NSMenuItem"; title = "Save…"; ObjectID = "pxx-59-PXV"; */ +"pxx-59-PXV.title" = "Save…"; + +/* Class = "NSMenuItem"; title = "Find Next"; ObjectID = "q09-fT-Sye"; */ +"q09-fT-Sye.title" = "Find Next"; + +/* Class = "NSTextFieldCell"; title = "Table View Cell"; ObjectID = "qG2-7c-SRN"; */ +"qG2-7c-SRN.title" = "Table View Cell"; + +/* Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "qIS-W8-SiK"; */ +"qIS-W8-SiK.title" = "Page Setup…"; + +/* Class = "NSTextFieldCell"; title = "Text Cell"; ObjectID = "qbS-Yb-jOG"; */ +"qbS-Yb-jOG.title" = "Text Cell"; + +/* Class = "NSMenuItem"; title = "Check Spelling While Typing"; ObjectID = "rbD-Rh-wIN"; */ +"rbD-Rh-wIN.title" = "Check Spelling While Typing"; + +/* Class = "NSMenuItem"; title = "Smart Dashes"; ObjectID = "rgM-f4-ycn"; */ +"rgM-f4-ycn.title" = "Smart Dashes"; + +/* Class = "NSMenuItem"; title = "Show Toolbar"; ObjectID = "snW-S8-Cw5"; */ +"snW-S8-Cw5.title" = "Show Toolbar"; + +/* Class = "NSMenuItem"; title = "Data Detectors"; ObjectID = "tRr-pd-1PS"; */ +"tRr-pd-1PS.title" = "Data Detectors"; + +/* Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "tXI-mr-wws"; */ +"tXI-mr-wws.title" = "Open Recent"; + +/* Class = "NSMenu"; title = "Kern"; ObjectID = "tlD-Oa-oAM"; */ +"tlD-Oa-oAM.title" = "Kern"; + +/* Class = "NSMenu"; title = "APIExample"; ObjectID = "uQy-DD-JDr"; */ +"uQy-DD-JDr.title" = "APIExample"; + +/* Class = "NSMenuItem"; title = "Cut"; ObjectID = "uRl-iY-unG"; */ +"uRl-iY-unG.title" = "Cut"; + +/* Class = "NSMenuItem"; title = "Paste Style"; ObjectID = "vKC-jM-MkH"; */ +"vKC-jM-MkH.title" = "Paste Style"; + +/* Class = "NSMenuItem"; title = "Show Ruler"; ObjectID = "vLm-3I-IUL"; */ +"vLm-3I-IUL.title" = "Show Ruler"; + +/* Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "vNY-rz-j42"; */ +"vNY-rz-j42.title" = "Clear Menu"; + +/* Class = "NSMenuItem"; title = "Make Upper Case"; ObjectID = "vmV-6d-7jI"; */ +"vmV-6d-7jI.title" = "Make Upper Case"; + +/* Class = "NSMenu"; title = "Ligatures"; ObjectID = "w0m-vy-SC9"; */ +"w0m-vy-SC9.title" = "Ligatures"; + +/* Class = "NSMenuItem"; title = "Align Right"; ObjectID = "wb2-vD-lq4"; */ +"wb2-vD-lq4.title" = "Align Right"; + +/* Class = "NSMenuItem"; title = "Help"; ObjectID = "wpr-3q-Mcd"; */ +"wpr-3q-Mcd.title" = "Help"; + +/* Class = "NSMenuItem"; title = "Copy"; ObjectID = "x3v-GG-iWU"; */ +"x3v-GG-iWU.title" = "Copy"; + +/* Class = "NSMenuItem"; title = "Use All"; ObjectID = "xQD-1f-W4t"; */ +"xQD-1f-W4t.title" = "Use All"; + +/* Class = "NSMenuItem"; title = "Speech"; ObjectID = "xrE-MZ-jX0"; */ +"xrE-MZ-jX0.title" = "Speech"; + +/* Class = "NSMenuItem"; title = "Show Substitutions"; ObjectID = "z6F-FW-3nz"; */ +"z6F-FW-3nz.title" = "Show Substitutions"; diff --git a/macOS/APIExampleTests/APIExampleTests.swift b/macOS/APIExampleTests/APIExampleTests.swift new file mode 100644 index 000000000..88e8fed62 --- /dev/null +++ b/macOS/APIExampleTests/APIExampleTests.swift @@ -0,0 +1,34 @@ +// +// APIExampleTests.swift +// APIExampleTests +// +// Created by 张乾泽 on 2020/8/28. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import XCTest +@testable import APIExample + +class APIExampleTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/macOS/APIExampleTests/Info.plist b/macOS/APIExampleTests/Info.plist new file mode 100644 index 000000000..64d65ca49 --- /dev/null +++ b/macOS/APIExampleTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/macOS/APIExampleUITests/APIExampleUITests.swift b/macOS/APIExampleUITests/APIExampleUITests.swift new file mode 100644 index 000000000..f4226a138 --- /dev/null +++ b/macOS/APIExampleUITests/APIExampleUITests.swift @@ -0,0 +1,43 @@ +// +// APIExampleUITests.swift +// APIExampleUITests +// +// Created by 张乾泽 on 2020/8/28. +// Copyright © 2020 Agora Corp. All rights reserved. +// + +import XCTest + +class APIExampleUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTOSSignpostMetric.applicationLaunch]) { + XCUIApplication().launch() + } + } + } +} diff --git a/macOS/APIExampleUITests/Info.plist b/macOS/APIExampleUITests/Info.plist new file mode 100644 index 000000000..64d65ca49 --- /dev/null +++ b/macOS/APIExampleUITests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/macOS/Podfile b/macOS/Podfile new file mode 100644 index 000000000..50d583159 --- /dev/null +++ b/macOS/Podfile @@ -0,0 +1,21 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +target 'APIExample' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + # Pods for APIExample + pod 'AGEVideoLayout', '~> 1.0.2' + pod 'AgoraRtcEngine_macOS', '3.3.0' + + target 'APIExampleTests' do + inherit! :search_paths + # Pods for testing + end + + target 'APIExampleUITests' do + # Pods for testing + end + +end diff --git a/macOS/cicd/build-template/build-ios.yml b/macOS/cicd/build-template/build-ios.yml new file mode 100644 index 000000000..b95e2ac99 --- /dev/null +++ b/macOS/cicd/build-template/build-ios.yml @@ -0,0 +1,37 @@ +parameters: + displayName: '' + workingDirectory: '' + project: '' + scheme: '' + +jobs: + - job: ${{ parameters.displayName }}Build + displayName: ${{ parameters.displayName }} + + pool: + vmImage: 'macOS-10.14' + + variables: + - group: AgoraKeys + + steps: + - script: cd '${{parameters.workingDirectory}}/cicd/scripts' && ls && python keycenter.py && ls + env: + AGORA_APP_ID: $(agora.appId) + File_Directory: '../../${{ parameters.workingDirectory }}/${{ parameters.project }}/Common' + + - task: InstallAppleCertificate@2 + inputs: + certSecureFile: 'certificate.p12' + certPwd: $(agora.password) + + - task: InstallAppleProvisioningProfile@1 + inputs: + provProfileSecureFile: 'AgoraAppsDevProfile.mobileprovision' + + - script: cd '${{parameters.workingDirectory}}/cicd/scripts' && chmod +x ios_build.sh && ./ios_build.sh ../../${{ parameters.workingDirectory }} ${{ parameters.project }} ${{ parameters.scheme }} + + - task: PublishBuildArtifacts@1 + inputs: + PathtoPublish: ${{ parameters.workingDirectory }}/app + ArtifactName: ${{ parameters.displayName }} diff --git a/macOS/cicd/build-template/build-mac.yml b/macOS/cicd/build-template/build-mac.yml new file mode 100644 index 000000000..34ce279f6 --- /dev/null +++ b/macOS/cicd/build-template/build-mac.yml @@ -0,0 +1,41 @@ +parameters: + displayName: '' + workingDirectory: '' + scheme: '' + sdkurl: '' + bundleid: '' + username: '' + password: '' + ascprovider: '' + +jobs: + - job: ${{ parameters.displayName }}Build + displayName: ${{ parameters.displayName }} + + pool: + vmImage: 'macOS-10.14' + + variables: + - group: AgoraKeys + + steps: + - script: cd '${{parameters.workingDirectory}}/cicd/scripts' && ls && python keycenter.py && ls + env: + AGORA_APP_ID: $(agora.appId) + File_Directory: '../../${{ parameters.project }}/Commons' + + - task: InstallAppleCertificate@2 + inputs: + certSecureFile: 'apiexamplemac.p12' + certPwd: $(agora.api.example.mac.cert.pass) + + - task: InstallAppleProvisioningProfile@1 + inputs: + provProfileSecureFile: 'apiexamplemac.provisionprofile' + + - script: cd '${{parameters.workingDirectory}}/cicd/scripts' && chmod +x mac_build.sh && ./mac_build.sh ../../ ${{ parameters.project }} ${{ parameters.scheme }} ${{parameters.bundleid}} ${{parameters.username}} $(agora.api.example.mac.notarize.pass) ${{parameters.ascprovider}} + + - task: PublishBuildArtifacts@1 + inputs: + PathtoPublish: ${{ parameters.workingDirectory }}/${{ parameters.scheme }}.zip + ArtifactName: ${{ parameters.displayName }} \ No newline at end of file diff --git a/macOS/cicd/build-template/github-release.yml b/macOS/cicd/build-template/github-release.yml new file mode 100644 index 000000000..46e5c7aa9 --- /dev/null +++ b/macOS/cicd/build-template/github-release.yml @@ -0,0 +1,3 @@ +parameters: + displayName: '' + diff --git a/macOS/cicd/scripts/ios_build.sh b/macOS/cicd/scripts/ios_build.sh new file mode 100755 index 000000000..6ba2a543f --- /dev/null +++ b/macOS/cicd/scripts/ios_build.sh @@ -0,0 +1,40 @@ +WORKING_PATH=$1 +APP_Project=$2 +APP_TARGET=$3 +MODE=Release + +echo "WORKING_PATH: ${WORKING_PATH}" +echo "APP_TARGET: ${APP_TARGET}" + +cd ${WORKING_PATH} +echo `pwd` + +rm -f *.ipa +rm -rf *.app +rm -f *.zip +rm -rf dSYMs +rm -rf *.dSYM +rm -f *dSYMs.zip +rm -rf *.xcarchive + +Export_Plist_File=exportPlist.plist + +BUILD_DATE=`date +%Y-%m-%d-%H.%M.%S` +ArchivePath=${APP_TARGET}-${BUILD_DATE}.xcarchive + +TARGET_FILE="" +if [ ! -f "Podfile" ];then +TARGET_FILE="${APP_Project}.xcodeproj" +xcodebuild clean -project ${TARGET_FILE} -scheme "${APP_TARGET}" -configuration ${MODE} +xcodebuild -project ${TARGET_FILE} -scheme "${APP_TARGET}" -configuration ${MODE} -archivePath ${ArchivePath} archive +else +pod install +TARGET_FILE="${APP_Project}.xcworkspace" +xcodebuild clean -workspace ${TARGET_FILE} -scheme "${APP_TARGET}" -configuration ${MODE} +xcodebuild -workspace ${TARGET_FILE} -scheme "${APP_TARGET}" -configuration ${MODE} -archivePath ${ArchivePath} archive +fi + +xcodebuild -exportArchive -exportOptionsPlist ${Export_Plist_File} -archivePath ${ArchivePath} -exportPath . + +mkdir app +mv *.ipa app && mv *.xcarchive app diff --git a/macOS/cicd/scripts/keycenter.py b/macOS/cicd/scripts/keycenter.py new file mode 100644 index 000000000..f900ddaf1 --- /dev/null +++ b/macOS/cicd/scripts/keycenter.py @@ -0,0 +1,50 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- +import re +import os + +def main(): + appId = "" + if "AGORA_APP_ID" in os.environ: + appId = os.environ["AGORA_APP_ID"] + token = "" + + fileDirectory = "" + if "File_Directory" in os.environ: + fileDirectory = os.environ["File_Directory"] + + # KeyCenter.swift + KeyCenterPath = fileDirectory + "/KeyCenter.swift" + print("KeyCenterPath: %s" %KeyCenterPath) + + try: + f = open(KeyCenterPath, 'r+') + content = f.read() + appString = "\"" + appId + "\"" + tokenString = "\"" + token + "\"" + contentNew = re.sub(r'<#Your App Id#>', appString, content) + contentNew = re.sub(r'<#Temp Access Token#>', tokenString, contentNew) + f.seek(0) + f.write(contentNew) + f.truncate() + except IOError: + print("Swift File is not accessible.") + + # KeyCenter.m + KeyCenterPath = fileDirectory + "/KeyCenter.m" + + try: + f = open(KeyCenterPath, 'r+') + content = f.read() + appString = "@\"" + appId + "\"" + tokenString = "@\"" + token + "\"" + contentNew = re.sub(r'<#Your App Id#>', appString, content) + contentNew = re.sub(r'<#Temp Access Token#>', tokenString, contentNew) + f.seek(0) + f.write(contentNew) + f.truncate() + except IOError: + print("OC File is not accessible.") + +if __name__ == "__main__": + main() diff --git a/macOS/cicd/scripts/mac_build.sh b/macOS/cicd/scripts/mac_build.sh new file mode 100755 index 000000000..2571933d7 --- /dev/null +++ b/macOS/cicd/scripts/mac_build.sh @@ -0,0 +1,47 @@ +WORKING_PATH=$1 +APP_Project=$2 +APP_TARGET=$3 +BUNDLE_ID=$4 +USERNAME=$5 +PASSWORD=$6 +ASCPROVIDER=$7 +MODE=Release + +echo "WORKING_PATH: ${WORKING_PATH}" +echo "APP_TARGET: ${APP_TARGET}" +echo "PROVIDER: ${ASCPROVIDER}" + +cd ${WORKING_PATH} +echo `pwd` + +rm -f *.ipa +rm -rf *.app +rm -f *.zip +rm -rf dSYMs +rm -rf *.dSYM +rm -f *dSYMs.zip +rm -rf *.xcarchive + +Export_Plist_File=exportPlist.plist + +BUILD_DATE=`date +%Y-%m-%d-%H.%M.%S` +ArchivePath=${APP_TARGET}-${BUILD_DATE}.xcarchive + +TARGET_FILE="" +if [ ! -f "Podfile" ];then +TARGET_FILE="${APP_Project}.xcodeproj" +xcodebuild clean -project ${TARGET_FILE} -scheme "${APP_TARGET}" -configuration ${MODE} +xcodebuild -project ${TARGET_FILE} -scheme "${APP_TARGET}" -configuration ${MODE} -archivePath ${ArchivePath} archive +else +pod install +TARGET_FILE="${APP_Project}.xcworkspace" +xcodebuild clean -workspace ${TARGET_FILE} -scheme "${APP_TARGET}" -configuration ${MODE} +xcodebuild -workspace ${TARGET_FILE} -scheme "${APP_TARGET}" -configuration ${MODE} -archivePath ${ArchivePath} archive +fi + +xcodebuild -exportArchive -exportOptionsPlist ${Export_Plist_File} -archivePath ${ArchivePath} -exportPath . + +ls -alt + +ditto -c -k --keepParent ${APP_TARGET}.app ${APP_TARGET}.zip +xcrun altool --notarize-app -f ${APP_TARGET}.zip --primary-bundle-id ${BUNDLE_ID} --asc-provider ${ASCPROVIDER} --username ${USERNAME} --password ${PASSWORD} \ No newline at end of file diff --git a/macOS/exportPlist.plist b/macOS/exportPlist.plist new file mode 100644 index 000000000..18c15564c --- /dev/null +++ b/macOS/exportPlist.plist @@ -0,0 +1,15 @@ + + + + + method + mac-application + compileBitcode + + provisioningProfiles + + io.agora.api.example.APIExample + apiexamplemac + + + \ No newline at end of file diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 000000000..46405cfc5 --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,261 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +.vscode/ + +/.#* +libs/ + +ThirdParty/ + +MediaPlayerPart/ \ No newline at end of file diff --git a/windows/APIExample/APIExample.sln b/windows/APIExample/APIExample.sln new file mode 100644 index 000000000..c1b350ec2 --- /dev/null +++ b/windows/APIExample/APIExample.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.852 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "APIExample", "APIExample\APIExample.vcxproj", "{DB16CA2F-3910-4449-A5BD-6A602B33BE0F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {DB16CA2F-3910-4449-A5BD-6A602B33BE0F}.Debug|x64.ActiveCfg = Debug|x64 + {DB16CA2F-3910-4449-A5BD-6A602B33BE0F}.Debug|x64.Build.0 = Debug|x64 + {DB16CA2F-3910-4449-A5BD-6A602B33BE0F}.Debug|x86.ActiveCfg = Debug|Win32 + {DB16CA2F-3910-4449-A5BD-6A602B33BE0F}.Debug|x86.Build.0 = Debug|Win32 + {DB16CA2F-3910-4449-A5BD-6A602B33BE0F}.Release|x64.ActiveCfg = Release|x64 + {DB16CA2F-3910-4449-A5BD-6A602B33BE0F}.Release|x64.Build.0 = Release|x64 + {DB16CA2F-3910-4449-A5BD-6A602B33BE0F}.Release|x86.ActiveCfg = Release|Win32 + {DB16CA2F-3910-4449-A5BD-6A602B33BE0F}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {789EBA9F-947D-4CFF-ADDB-AE893C262318} + EndGlobalSection +EndGlobal diff --git a/windows/APIExample/APIExample/AGVideoTestWnd.cpp b/windows/APIExample/APIExample/AGVideoTestWnd.cpp new file mode 100644 index 000000000..5683160f5 --- /dev/null +++ b/windows/APIExample/APIExample/AGVideoTestWnd.cpp @@ -0,0 +1,120 @@ +#include "stdafx.h" +#include "AGVideoTestWnd.h" + + +// CAGVideoTestWnd + +IMPLEMENT_DYNAMIC(CAGVideoTestWnd, CWnd) + +CAGVideoTestWnd::CAGVideoTestWnd() +: m_nVolRange(255) +, m_nCurVol(0) +, m_crVolbarFreeColor(RGB(32, 32, 32)) +, m_crVolbarBusyColor(RGB(208, 208, 208)) +, m_crVolbarBackColor(RGB(0x26, 0x26, 0x26)) +, m_crBackColor(RGB(0x70, 0x70, 0x70)) +, m_nVolbarWidth(15) +{ + +} + +CAGVideoTestWnd::~CAGVideoTestWnd() +{ +} + + +BEGIN_MESSAGE_MAP(CAGVideoTestWnd, CWnd) + ON_WM_PAINT() + ON_WM_CREATE() + ON_WM_SIZE() +END_MESSAGE_MAP() + + + +// CAGVideoTestWnd Message handle + +int CAGVideoTestWnd::OnCreate(LPCREATESTRUCT lpCreateStruct) +{ + if (CWnd::OnCreate(lpCreateStruct) == -1) + return -1; + + // TODO: add you own creation code here + CRect rcChildRect; + + DWORD dwWndStyle = WS_VISIBLE | WS_CHILD; + rcChildRect.SetRect(15, 0, lpCreateStruct->cx-30, lpCreateStruct->cy); + m_wndVideoWnd.Create(NULL, _T("AgoraVideoWnd"), dwWndStyle, rcChildRect, this, IDC_STATIC); + + return 0; +} + +void CAGVideoTestWnd::OnPaint() +{ + // TODO: add message handle code here + CPaintDC dc(this); + + CRect rcClient; + + GetClientRect(&rcClient); + dc.FillSolidRect(0, 0, rcClient.Width(), rcClient.Height(), m_crBackColor); + + dc.FillSolidRect(0, 0, m_nVolbarWidth, rcClient.Height(), m_crVolbarBackColor); + dc.FillSolidRect(rcClient.Width() - m_nVolbarWidth, 0, m_nVolbarWidth, rcClient.Height(), m_crVolbarBackColor); + + int nMarkCount = rcClient.Height() / 5; + int nTopPoint = m_nCurVol*nMarkCount / m_nVolRange; + + for (int nIndex = 0; nIndex < nMarkCount; nIndex++) { + if (nIndex <= nTopPoint) { + dc.FillSolidRect(0, rcClient.bottom - 5 * nIndex - 3, m_nVolbarWidth, 3, m_crVolbarBusyColor); + dc.FillSolidRect(rcClient.Width() - m_nVolbarWidth, rcClient.bottom - 5 * nIndex - 3, m_nVolbarWidth, 3, m_crVolbarBusyColor); + } + else { + dc.FillSolidRect(0, rcClient.bottom - 5 * nIndex - 3, m_nVolbarWidth, 3, m_crVolbarFreeColor); + dc.FillSolidRect(rcClient.Width() - m_nVolbarWidth, rcClient.bottom - 5 * nIndex - 3, m_nVolbarWidth, 3, m_crVolbarFreeColor); + } + } +} + + +void CAGVideoTestWnd::SetVolRange(int nRange) +{ + if (nRange > 100 || nRange < 0) + nRange = 100; + + m_nVolRange = nRange; + + Invalidate(FALSE); +} + +void CAGVideoTestWnd::SetCurVol(int nCurVol) +{ + if (nCurVol < 0 || nCurVol > m_nVolRange) + nCurVol = 0; + CRect rcClient; + GetClientRect(&rcClient); + m_nCurVol = nCurVol; + RECT lrc; + lrc.left = 0; + lrc.right = m_nVolbarWidth; + lrc.top = 0; + lrc.bottom = rcClient.Height(); + InvalidateRect(&lrc); + RECT rrc; + rrc.left = rcClient.Width() - m_nVolbarWidth; + rrc.right = rcClient.Width(); + rrc.top = 0; + rrc.bottom = rcClient.Height(); + InvalidateRect(&rrc); +} + + +void CAGVideoTestWnd::OnSize(UINT nType, int cx, int cy) +{ + CWnd::OnSize(nType, cx, cy); + + if (m_wndVideoWnd.GetSafeHwnd() != NULL) + m_wndVideoWnd.MoveWindow(15, 0, cx - 30, cy); + + // TODO: add message handle code here +} diff --git a/windows/APIExample/APIExample/AGVideoTestWnd.h b/windows/APIExample/APIExample/AGVideoTestWnd.h new file mode 100644 index 000000000..8d2a7fa79 --- /dev/null +++ b/windows/APIExample/APIExample/AGVideoTestWnd.h @@ -0,0 +1,45 @@ +#pragma once + + +// CAGVideoTestWnd + +class CAGVideoTestWnd : public CWnd +{ + DECLARE_DYNAMIC(CAGVideoTestWnd) + +public: + CAGVideoTestWnd(); + virtual ~CAGVideoTestWnd(); + + HWND GetVideoSafeHwnd() { return m_wndVideoWnd.GetSafeHwnd(); }; + + // ָʾ + + void SetVolbarColor(DWORD dwFreeColor = RGB(184, 184, 184), DWORD dwBusyColor = RGB(0, 255, 0), DWORD dwBackColor = RGB(0, 0, 0)); // 趨ɫ + void SetVolRange(int nRange = 100); + void SetCurVol(int nCurVol = 0); + +protected: + afx_msg void OnPaint(); + afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); + + DECLARE_MESSAGE_MAP() + +private: + CWnd m_wndVideoWnd; // the wnd for show video + + int m_nVolbarWidth; + + int m_nVolbarPos; // the vol bar pos + int m_nVolRange; // the max vol + int m_nCurVol; // the current vol + + COLORREF m_crBackColor; + COLORREF m_crVolbarFreeColor; + COLORREF m_crVolbarBusyColor; + COLORREF m_crVolbarBackColor; +public: + afx_msg void OnSize(UINT nType, int cx, int cy); +}; + + diff --git a/windows/APIExample/APIExample/AGVideoWnd.cpp b/windows/APIExample/APIExample/AGVideoWnd.cpp new file mode 100644 index 000000000..cb853e7be --- /dev/null +++ b/windows/APIExample/APIExample/AGVideoWnd.cpp @@ -0,0 +1,307 @@ +// AGVideoWnd.cpp : implement file +// + +#include "stdafx.h" +#include "APIExample.h" +#include "AGVideoWnd.h" + +IMPLEMENT_DYNAMIC(CAGInfoWnd, CWnd) + +CAGInfoWnd::CAGInfoWnd() +: m_bShowTip(TRUE) +, m_nWidth(0) +, m_nHeight(0) +, m_nFps(0) +, m_nBitrate(0) +{ + m_brBack.CreateSolidBrush(RGB(0x00, 0xA0, 0xE9)); +} + +CAGInfoWnd::~CAGInfoWnd() +{ + m_brBack.DeleteObject(); +} + + +BEGIN_MESSAGE_MAP(CAGInfoWnd, CWnd) + ON_WM_PAINT() + ON_WM_ERASEBKGND() +END_MESSAGE_MAP() + + +void CAGInfoWnd::ShowTips(BOOL bShow) +{ + m_bShowTip = bShow; + + if (bShow) + ShowWindow(SW_SHOW); + else + ShowWindow(SW_HIDE); + + Invalidate(FALSE); +} + +void CAGInfoWnd::SetVideoResolution(int nWidth, int nHeight) +{ + m_nWidth = nWidth; + m_nHeight = nHeight; + + if (m_bShowTip) { + Invalidate(TRUE); + UpdateWindow(); + } +} + +void CAGInfoWnd::SetFrameRateInfo(int nFPS) +{ + m_nFps = nFPS; + + if (m_bShowTip) { + Invalidate(TRUE); + UpdateWindow(); + } +} + +void CAGInfoWnd::SetBitrateInfo(int nBitrate) +{ + m_nBitrate = nBitrate; + + if (m_bShowTip) { + Invalidate(TRUE); + UpdateWindow(); + } +} + +void CAGInfoWnd::OnPaint() +{ + CPaintDC dc(this); + CRect rcClient; + CString strTip; + + dc.SetBkMode(TRANSPARENT); + dc.SetTextColor(RGB(0xFF, 0xFF, 0xFF)); + + if (m_bShowTip) { + // 640x480,15fps,400k + GetClientRect(&rcClient); + rcClient.top += 4; + strTip.Format(_T("%dx%d, %dfps, %dK \n %u"), m_nWidth, m_nHeight, m_nFps, m_nBitrate, m_nUID); + dc.DrawText(strTip, &rcClient, DT_VCENTER | DT_CENTER); + } +} + +BOOL CAGInfoWnd::OnEraseBkgnd(CDC* pDC) +{ + // TODO: add message handle code and /or call defalut values here + CRect rcClient; + + GetClientRect(&rcClient); + pDC->FillRect(&rcClient, &m_brBack); + + return TRUE; +} + +// CAGVideoWnd + +IMPLEMENT_DYNAMIC(CAGVideoWnd, CWnd) + +CAGVideoWnd::CAGVideoWnd() +: m_nUID(0) +, m_crBackColor(RGB(0x58, 0x58, 0x58)) +, m_bShowVideoInfo(FALSE) +, m_bBigShow(FALSE) +{ + +} + +CAGVideoWnd::~CAGVideoWnd() +{ + m_imgBackGround.DeleteImageList(); +} + + +BEGIN_MESSAGE_MAP(CAGVideoWnd, CWnd) + ON_WM_ERASEBKGND() + ON_WM_LBUTTONDOWN() + ON_WM_RBUTTONDOWN() + ON_WM_CREATE() + ON_WM_PAINT() + ON_WM_SIZE() + ON_WM_LBUTTONDBLCLK() +END_MESSAGE_MAP() + + + +// CAGVideoWnd message handle +BOOL CAGVideoWnd::OnEraseBkgnd(CDC* pDC) +{ + // TODO: add message handle code and /or call default values here + CRect rcClient; + CPoint ptDraw; + //IMAGEINFO imgInfo; + + GetClientRect(&rcClient); + + pDC->FillSolidRect(&rcClient, m_crBackColor); + /*if (!m_imgBackGround.GetImageInfo(0, &imgInfo)) + return TRUE; + + ptDraw.SetPoint((rcClient.Width() - imgInfo.rcImage.right) / 2, (rcClient.Height() - imgInfo.rcImage.bottom) / 2); + if (ptDraw.x < 0) + ptDraw.x = 0; + if (ptDraw.y <= 0) + ptDraw.y = 0; + + m_imgBackGround.Draw(pDC, 0, ptDraw, ILD_NORMAL);*/ + return TRUE; +} + +void CAGVideoWnd::SetUID(UINT nUID) +{ + m_nUID = nUID; + + m_wndInfo.SetUID(nUID); + //m_wndInfo.ShowWindow(SW_SHOW); +} + +UINT CAGVideoWnd::GetUID() +{ + return m_nUID; +} + +BOOL CAGVideoWnd::IsWndFree() +{ + return m_nUID == 0 ? TRUE : FALSE; +} + +BOOL CAGVideoWnd::SetBackImage(UINT nID, UINT nWidth, UINT nHeight, COLORREF crMask) +{ + CBitmap bmBackImage; + + if (!bmBackImage.LoadBitmap(nID)) + return FALSE; + + m_imgBackGround.DeleteImageList(); + + m_imgBackGround.Create(nWidth, nHeight, ILC_COLOR24 | ILC_MASK, 1, 1); + m_imgBackGround.Add(&bmBackImage, crMask); + bmBackImage.DeleteObject(); + + Invalidate(TRUE); + + return TRUE; +} + +void CAGVideoWnd::SetFaceColor(COLORREF crBackColor) +{ + m_crBackColor = crBackColor; + + Invalidate(TRUE); +} + +void CAGVideoWnd::SetVideoResolution(UINT nWidth, UINT nHeight) +{ + m_nWidth = nWidth; + m_nHeight = nHeight; + + m_wndInfo.SetVideoResolution(nWidth, nHeight); +} + +void CAGVideoWnd::GetVideoResolution(UINT *nWidth, UINT *nHeight) +{ + *nWidth = m_nWidth; + *nHeight = m_nHeight; +} + +void CAGVideoWnd::SetBitrateInfo(int nReceivedBitrate) +{ + m_nBitRate = nReceivedBitrate; + m_wndInfo.SetBitrateInfo(nReceivedBitrate); +} + +void CAGVideoWnd::SetFrameRateInfo(int nReceiveFrameRate) +{ + m_nFrameRate = nReceiveFrameRate; + m_wndInfo.SetFrameRateInfo(nReceiveFrameRate); +} + +void CAGVideoWnd::OnLButtonDown(UINT nFlags, CPoint point) +{ + // TODO: add message handle code and /or call default values here + + ::SendMessage(GetParent()->GetSafeHwnd(), WM_SHOWBIG, (WPARAM)this, (LPARAM)m_nUID); + + CWnd::OnLButtonDown(nFlags, point); +} + + +void CAGVideoWnd::OnRButtonDown(UINT nFlags, CPoint point) +{ + // TODO: add message handle code and /or call default values here + ::SendMessage(GetParent()->GetSafeHwnd(), WM_SHOWMODECHANGED, (WPARAM)this, (LPARAM)m_nUID); + + CWnd::OnRButtonDown(nFlags, point); +} + + +int CAGVideoWnd::OnCreate(LPCREATESTRUCT lpCreateStruct) +{ + if (CWnd::OnCreate(lpCreateStruct) == -1) + return -1; + + // TODO: add you own creation code here + m_wndInfo.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, WND_INFO_WIDTH, WND_INFO_HEIGHT), this, IDC_STATIC); + m_wndInfo.ShowWindow(SW_HIDE); + return 0; +} + + +void CAGVideoWnd::ShowVideoInfo(BOOL bShow) +{ + m_bShowVideoInfo = bShow; + + m_wndInfo.ShowTips(bShow); + Invalidate(TRUE); +} + +void CAGVideoWnd::SetBigShowFlag(BOOL bBigShow) +{ + CRect rcClient; + + m_bBigShow = bBigShow; + GetClientRect(&rcClient); + + int x = (rcClient.Width()- WND_INFO_WIDTH) / 2; + int y = rcClient.Height() - WND_INFO_HEIGHT; + + if (m_wndInfo.GetSafeHwnd() != NULL) { + if (m_bBigShow) + y -= 4; + + m_wndInfo.MoveWindow(x, y, WND_INFO_WIDTH, WND_INFO_HEIGHT); + } +}; + + +void CAGVideoWnd::OnSize(UINT nType, int cx, int cy) +{ + CWnd::OnSize(nType, cx, cy); + + int x = (cx - WND_INFO_WIDTH) / 2; + int y = cy - WND_INFO_HEIGHT; + // TODO: add message handle code here + if (m_wndInfo.GetSafeHwnd() != NULL) { + if (m_bBigShow) + y -= 4; + + m_wndInfo.MoveWindow(x, y, WND_INFO_WIDTH, WND_INFO_HEIGHT); + } +} + +void CAGVideoWnd::OnLButtonDblClk(UINT nFlags, CPoint point) +{ + // TODO: add message handle code and /or call default values here + ::SendMessage(GetParent()->GetSafeHwnd(), WM_SHOWMODECHANGED, (WPARAM)this, (LPARAM)m_nUID); + + CWnd::OnLButtonDblClk(nFlags, point); +} diff --git a/windows/APIExample/APIExample/AGVideoWnd.h b/windows/APIExample/APIExample/AGVideoWnd.h new file mode 100644 index 000000000..bb443e06f --- /dev/null +++ b/windows/APIExample/APIExample/AGVideoWnd.h @@ -0,0 +1,105 @@ +#pragma once + +#define WM_SHOWMODECHANGED WM_USER+300 +#define WM_SHOWBIG WM_USER+301 + +#define WND_VIDEO_WIDTH 192 +#define WND_VIDEO_HEIGHT 144 + +#define WND_INFO_WIDTH 192 +#define WND_INFO_HEIGHT 36 + +class CAGInfoWnd : public CWnd +{ + DECLARE_DYNAMIC(CAGInfoWnd) + +public: + CAGInfoWnd(); + virtual ~CAGInfoWnd(); + + void ShowTips(BOOL bShow = TRUE); + void SetVideoResolution(int nWidth, int nHeight); + void SetFrameRateInfo(int nFPS); + void SetBitrateInfo(int nBitrate); + + void SetUID(UINT dwUID) { m_nUID = dwUID; } +protected: + afx_msg void OnPaint(); + afx_msg BOOL OnEraseBkgnd(CDC* pDC); + + DECLARE_MESSAGE_MAP() + +private: + BOOL m_bShowTip; + + COLORREF m_crBackColor; + + int m_nWidth; + int m_nHeight; + int m_nFps; + int m_nBitrate; + + CBrush m_brBack; + UINT m_nUID = 0; +}; + + +class CAGVideoWnd : public CWnd +{ + DECLARE_DYNAMIC(CAGVideoWnd) + +public: + CAGVideoWnd(); + virtual ~CAGVideoWnd(); + + void SetUID(UINT dwUID); + + UINT GetUID(); + BOOL IsWndFree(); + + void SetFaceColor(COLORREF crBackColor); + BOOL SetBackImage(UINT nID, UINT nWidth, UINT nHeight, COLORREF crMask = RGB(0xFF, 0xff, 0xFF)); + + void SetVideoResolution(UINT nWidth, UINT nHeight); + void GetVideoResolution(UINT *nWidth, UINT *nHeight); + + void SetBitrateInfo(int nReceivedBitrate); + int GetBitrateInfo() { return m_nBitRate; }; + + void SetFrameRateInfo(int nReceiveFrameRate); + int GetFrameRateInfo() { return m_nFrameRate; }; + + void ShowVideoInfo(BOOL bShow); + BOOL IsVideoInfoShowed() { return m_bShowVideoInfo; }; + + void SetBigShowFlag(BOOL bBigShow); + BOOL IsBigShow() { return m_bBigShow; }; + +protected: + afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); + afx_msg BOOL OnEraseBkgnd(CDC* pDC); + afx_msg void OnLButtonDown(UINT nFlags, CPoint point); + afx_msg void OnLButtonDblClk(UINT nFlags, CPoint point); + afx_msg void OnRButtonDown(UINT nFlags, CPoint point); + afx_msg void OnSize(UINT nType, int cx, int cy); + + DECLARE_MESSAGE_MAP() + +private: + CImageList m_imgBackGround; + COLORREF m_crBackColor; + + CAGInfoWnd m_wndInfo; + +private: + UINT m_nUID = 0; + + UINT m_nWidth; + UINT m_nHeight; + int m_nFrameRate; + int m_nBitRate; + BOOL m_bShowVideoInfo; + BOOL m_bBigShow; +}; + + diff --git a/windows/APIExample/APIExample/APIExample.cpp b/windows/APIExample/APIExample/APIExample.cpp new file mode 100644 index 000000000..8d7fe29c0 --- /dev/null +++ b/windows/APIExample/APIExample/APIExample.cpp @@ -0,0 +1,105 @@ + +// APIExample.cpp : Defines the class behaviors for the application. +// + +#include "stdafx.h" +#include "APIExample.h" +#include "APIExampleDlg.h" + +#ifdef _DEBUG +#define new DEBUG_NEW +#endif + +#include "CConfig.h" +// CAPIExampleApp + +BEGIN_MESSAGE_MAP(CAPIExampleApp, CWinApp) + ON_COMMAND(ID_HELP, &CWinApp::OnHelp) +END_MESSAGE_MAP() + + +// CAPIExampleApp construction + +CAPIExampleApp::CAPIExampleApp() +{ + // support Restart Manager + m_dwRestartManagerSupportFlags = AFX_RESTART_MANAGER_SUPPORT_RESTART; + CConfig::GetInstance(); + // TODO: add construction code here, + // Place all significant initialization in InitInstance +} + + +// The one and only CAPIExampleApp object + +CAPIExampleApp theApp; + + +// CAPIExampleApp initialization + +BOOL CAPIExampleApp::InitInstance() +{ + // InitCommonControlsEx() is required on Windows XP if an application + // manifest specifies use of ComCtl32.dll version 6 or later to enable + // visual styles. Otherwise, any window creation will fail. + INITCOMMONCONTROLSEX InitCtrls; + InitCtrls.dwSize = sizeof(InitCtrls); + // Set this to include all the common control classes you want to use + // in your application. + InitCtrls.dwICC = ICC_WIN95_CLASSES; + InitCommonControlsEx(&InitCtrls); + + CWinApp::InitInstance(); + + AfxEnableControlContainer(); + InitKeyInfomation(); + // Create the shell manager, in case the dialog contains + // any shell tree view or shell list view controls. + CShellManager *pShellManager = new CShellManager; + + // Activate "Windows Native" visual manager for enabling themes in MFC controls + CMFCVisualManager::SetDefaultManager(RUNTIME_CLASS(CMFCVisualManagerWindows)); + + // Standard initialization + // If you are not using these features and wish to reduce the size + // of your final executable, you should remove from the following + // the specific initialization routines you do not need + // Change the registry key under which our settings are stored + // TODO: You should modify this string to be something appropriate + // such as the name of your company or organization + SetRegistryKey(_T("Local AppWizard-Generated Applications")); + + CAPIExampleDlg dlg; + m_pMainWnd = &dlg; + INT_PTR nResponse = dlg.DoModal(); + if (nResponse == IDOK) + { + // TODO: Place code here to handle when the dialog is + // dismissed with OK + } + else if (nResponse == IDCANCEL) + { + // TODO: Place code here to handle when the dialog is + // dismissed with Cancel + } + else if (nResponse == -1) + { + TRACE(traceAppMsg, 0, "Warning: dialog creation failed, so application is terminating unexpectedly.\n"); + TRACE(traceAppMsg, 0, "Warning: if you are using MFC controls on the dialog, you cannot #define _AFX_NO_MFC_CONTROLS_IN_DIALOGS.\n"); + } + + // Delete the shell manager created above. + if (pShellManager != nullptr) + { + delete pShellManager; + } + +#if !defined(_AFXDLL) && !defined(_AFX_NO_MFC_CONTROLS_IN_DIALOGS) + ControlBarCleanUp(); +#endif + + // Since the dialog has been closed, return FALSE so that we exit the + // application, rather than start the application's message pump. + return FALSE; +} + diff --git a/windows/APIExample/APIExample/APIExample.h b/windows/APIExample/APIExample/APIExample.h new file mode 100644 index 000000000..edcf1218b --- /dev/null +++ b/windows/APIExample/APIExample/APIExample.h @@ -0,0 +1,31 @@ + +// APIExample.h : main header file for the PROJECT_NAME application +// + +#pragma once + +#ifndef __AFXWIN_H__ + #error "include 'stdafx.h' before including this file for PCH" +#endif + +#include "resource.h" // main symbols + +// CAPIExampleApp: +// See APIExample.cpp for the implementation of this class +// + +class CAPIExampleApp : public CWinApp +{ +public: + CAPIExampleApp(); + +// Overrides +public: + virtual BOOL InitInstance(); + +// Implementation + + DECLARE_MESSAGE_MAP() +}; + +extern CAPIExampleApp theApp; diff --git a/windows/APIExample/APIExample/APIExample.rc b/windows/APIExample/APIExample/APIExample.rc new file mode 100644 index 000000000..67ef4fb70 --- /dev/null +++ b/windows/APIExample/APIExample/APIExample.rc @@ -0,0 +1,1048 @@ +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#ifndef APSTUDIO_INVOKED +#include "targetver.h" +#endif +#include "afxres.h" +#include "verrsrc.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// (壬й) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS) +LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED +#pragma code_page(936) + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#ifndef APSTUDIO_INVOKED\r\n" + "#include ""targetver.h""\r\n" + "#endif\r\n" + "#include ""afxres.h""\r\n" + "#include ""verrsrc.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "#define _AFX_NO_SPLITTER_RESOURCES\r\n" + "#define _AFX_NO_OLE_RESOURCES\r\n" + "#define _AFX_NO_TRACKER_RESOURCES\r\n" + "#define _AFX_NO_PROPERTY_RESOURCES\r\n" + "\r\n" + "#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\r\n" + "LANGUAGE 9, 1\r\n" + "#include ""res\\APIExample.rc2"" // non-Microsoft Visual C++ edited resources\r\n" + "#include ""afxres.rc"" // Standard components\r\n" + "#if !defined(_AFXDLL)\r\n" + "#include ""afxribbon.rc"" // MFC ribbon and control bar resources\r\n" + "#endif\r\n" + "#endif\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDR_MAINFRAME ICON "res\\APIExample.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog +// + +IDD_DIALOG_LIVEBROADCASTING DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310 + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + COMBOBOX IDC_COMBO_ROLE,56,348,60,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_COMBO_PERSONS,182,348,60,30,CBS_DROPDOWNLIST | WS_VSCROLL + LTEXT "Client Role",IDC_STATIC_ROLE,8,351,44,10 + LTEXT "Persons",IDC_STATIC_PERSONS,141,351,37,8 + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,259,350,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,319,348,218,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,555,348,50,14 + LTEXT "",IDC_STATIC_DETAIL,23,370,456,27 +END + +IDD_DIALOG_RTMPINJECT DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,19,339,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,79,337,218,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,306,337,50,14 + LTEXT "Inject URL",IDC_STATIC_INJECT_URL,20,361,48,8 + EDITTEXT IDC_EDIT_INJECT_URL,80,359,218,13,ES_AUTOHSCROLL + PUSHBUTTON "InjectUrl",IDC_BUTTON_ADDSTREAM,307,359,50,14 + LTEXT "",IDC_STATIC_DETAIL,442,325,181,58 +END + +IDD_DIALOG_RTMP_STREAMING DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,328,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,71,326,218,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,307,326,50,14 + LTEXT "Publish URL",IDC_STATIC_RTMP_URL,11,351,48,8 + EDITTEXT IDC_EDIT_RTMP_URL,71,349,218,13,ES_AUTOHSCROLL + PUSHBUTTON "AddStream",IDC_BUTTON_ADDSTREAM,307,349,50,14 + LTEXT "Publish URL",IDC_STATIC_RTMP_URLS,11,373,48,8 + COMBOBOX IDC_COMBO_RTMPURLS,70,370,218,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "RemoveStream",IDC_BUTTON_REMOVE_STREAM,307,370,62,14 + PUSHBUTTON "RemoveAll",IDC_BUTTON_REMOVE_ALLSTREAM,376,370,55,15 + LTEXT "",IDC_STATIC_DETAIL,442,325,181,58 + CONTROL "Check1",IDC_CHK_TRANS_CODING,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,373,349,63,10 +END + +IDD_DIALOG_METADATA DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,328,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,71,326,242,15,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,322,326,50,14 + LTEXT "Send SEI",IDC_STATIC_SENDSEI,11,353,48,8 + EDITTEXT IDC_EDIT_SEI,71,351,244,21,ES_MULTILINE | ES_AUTOVSCROLL + PUSHBUTTON "Send",IDC_BUTTON_SEND,325,350,50,14 + EDITTEXT IDC_EDIT_RECV,11,377,419,20,ES_MULTILINE | WS_DISABLED + LTEXT "",IDC_STATIC_METADATA_INFO,493,321,137,16 + PUSHBUTTON "Clear",IDC_BUTTON_CLEAR,385,351,50,14 +END + +IDD_DIALOG_SCREEN_SHARE DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,481,312,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,308,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,319,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,68,317,144,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,219,317,50,14 + LTEXT "Window HWND",IDC_STATIC_SCREEN_CAPTURE,11,340,54,8 + PUSHBUTTON "Share Window",IDC_BUTTON_START_CAPUTRE,219,338,50,14 + COMBOBOX IDC_COMBO_SCREEN_CAPTURE,68,338,144,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + LTEXT "",IDC_STATIC_DETAIL,491,321,132,62 + CONTROL "Share Cursor",IDC_CHECK_CURSOR,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,278,361,58,10 + GROUPBOX "General Settings",IDC_STATIC_GENERAL,274,312,216,66 + LTEXT "FPS",IDC_STATIC_FPS,277,325,21,10 + EDITTEXT IDC_EDIT_FPS,302,323,55,12,ES_AUTOHSCROLL + LTEXT "bitrate",IDC_STATIC_BITRATE,375,325,27,9 + EDITTEXT IDC_EDIT_BITRATE,406,323,46,14,ES_AUTOHSCROLL + PUSHBUTTON "Update Calpture Param",IDC_BUTTON_UPDATEPARAM,402,359,86,14 + LTEXT "Screen",IDC_STATIC_SCREEN_SHARE,11,359,48,8 + COMBOBOX IDC_COMBO_SCREEN_SCREEN,68,359,144,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "Share Screen",IDC_BUTTON_START_SHARE_SCREEN,219,359,50,14 + LTEXT "",IDC_STATIC_SCREEN_INFO,8,382,305,8 + LTEXT "",IDC_STATIC_SCREEN_INFO2,325,383,151,14 + CONTROL "WND FUCS",IDC_CHECK_WINDOW_FOCUS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,339,361,51,10 + LTEXT "ExcludeWindowList",IDC_STATIC_WND_LIST,280,344,62,10 + COMBOBOX IDC_COMBO_EXLUDE_WINDOW_LIST,343,343,144,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP +END + +IDD_DIALOG_CUSTOM_CAPTURE_VIDEO DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,328,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,71,326,218,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,307,326,50,14 + LTEXT "Caputre Video",IDC_STATIC_CAPTUREDEVICE,12,353,48,8 + PUSHBUTTON "Start Capture",IDC_BUTTON_START_CAPUTRE,384,352,50,14 + COMBOBOX IDC_COMBO_CAPTURE_VIDEO_DEVICE,71,353,149,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + LTEXT "",IDC_STATIC_DETAIL,442,325,181,58 + COMBOBOX IDC_COMBO_CAPTURE_VIDEO_TYPE,225,353,149,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP +END + +IDD_DIALOG_CUSTOM_CAPTURE_AUDIO DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,328,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,71,326,218,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,307,326,50,14 + LTEXT "Audio Device",IDC_STATIC_CAPTUREDEVICE,12,361,48,8 + PUSHBUTTON "Start Capture",IDC_BUTTON_START_CAPUTRE,384,352,50,14 + COMBOBOX IDC_COMBO_CAPTURE_AUDIO_DEVICE,71,361,149,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + LTEXT "",IDC_STATIC_DETAIL,442,325,181,58 + COMBOBOX IDC_COMBO_CAPTURE_AUDIO_TYPE,225,361,149,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "Start Capture",IDC_BUTTON_RENDER_AUDIO,384,368,50,14 +END + +IDD_DIALOG_MULTI_CHANNEL DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,328,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,71,326,218,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,307,326,50,14 + LTEXT "ChannelList",IDC_STATIC_CHANNEL_LIST,11,353,53,8 + COMBOBOX IDC_COMBO_CHANNEL_LIST,71,352,218,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + LTEXT "",IDC_STATIC_DETAIL,442,325,181,58 + PUSHBUTTON "JoinChannel",IDC_BUTTON_LEAVE_CHANNEL,308,353,50,14 +END + +IDD_DIALOG_AUDIO_PROFILE DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,328,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,71,326,218,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,307,326,60,14 + LTEXT "profile",IDC_STATIC_ADUIO_PROFILE,11,352,48,8 + PUSHBUTTON "Set AudioProfile",IDC_BUTTON_SET_AUDIO_PROFILE,307,361,60,14 + COMBOBOX IDC_COMBO_AUDIO_PROFILE,71,350,218,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + LTEXT "",IDC_STATIC_DETAIL,442,325,181,58 + LTEXT "scenario",IDC_STATIC_ADUIO_SCENARIO,10,374,48,8 + COMBOBOX IDC_COMBO_AUDIO_SCENARIO,71,372,218,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP +END + +IDD_DIALOG_BEAUTY_AUDIO DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,328,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,71,326,218,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,307,326,60,14 + LTEXT "Audio Change",IDC_STATIC_AUDIO_CHANGER,11,352,48,8 + COMBOBOX IDC_COMBO_AUDIO_CHANGER,71,350,172,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + LTEXT "",IDC_STATIC_DETAIL,456,325,167,58 + LTEXT "Reverb Preset",IDC_STATIC_BEAUTY_AUDIO_TYPE,12,374,48,8 + COMBOBOX IDC_COMBO_AUDIO_PERVERB_PRESET,71,373,171,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "Button2",IDC_BUTTON_SET_BEAUTY_AUDIO,355,358,58,14 + EDITTEXT IDC_EDIT_PARAM1,292,349,56,14,ES_AUTOHSCROLL + LTEXT "param1",IDC_STATIC_PARAM1,251,351,36,8 + LTEXT "param2",IDC_STATIC_PARAM2,251,373,38,8 + EDITTEXT IDC_EDIT_PARAM2,292,371,56,14,ES_AUTOHSCROLL +END + +IDD_DIALOG_AUDIO_MIX DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,328,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,71,326,218,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,307,326,60,14 + LTEXT "Audio Change",IDC_STATIC_AUDIO_MIX,11,345,48,8 + PUSHBUTTON "Set AudioChange",IDC_BUTTON_SET_AUDIO_MIX,307,344,60,14 + LTEXT "",IDC_STATIC_DETAIL,442,325,181,58 + EDITTEXT IDC_EDIT_AUDIO_MIX_PATH,71,345,218,13,ES_AUTOHSCROLL + CONTROL "only local play",IDC_CHK_ONLY_LOCAL,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,307,365,61,10 + CONTROL "replace microphone",IDC_CHK_REPLACE_MICROPHONE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,374,365,78,10 + LTEXT "repeat times",IDC_STATIC_AUDIO_REPEAT,11,363,48,8 + EDITTEXT IDC_EDIT_AUDIO_REPEAT_TIMES,71,363,218,13,ES_AUTOHSCROLL + CONTROL "",IDC_SLIDER_VOLUME,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,66,381,224,15 + LTEXT "repeat times",IDC_STATIC_AUDIO_VOLUME,11,383,48,8 +END + +IDD_DIALOG_ORIGINAL_VIDEO DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,328,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,71,326,218,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,307,326,50,14 + LTEXT "Caputre Video",IDC_STATIC_ORIGINAL_VIDEO_PROC,12,353,48,8 + PUSHBUTTON "Start Capture",IDC_BUTTON_SET_ORIGINAL_PROC,307,353,50,14 + COMBOBOX IDC_COMBO_ORIGINAL_VIDEO_PROC,71,353,218,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + LTEXT "",IDC_STATIC_DETAIL,442,325,181,58 +END + +IDD_DIALOG_CUSTOM_ENCRYPT DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,328,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,71,326,218,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,307,326,50,14 + LTEXT "Caputre Video",IDC_STATIC_CUSTOM_ENCRYPT,12,353,48,8 + PUSHBUTTON "Start Capture",IDC_BUTTON_SET_CUSTOM_ENCRYPT,307,353,50,14 + COMBOBOX IDC_COMBO_CUSTOM_ENCRYPT,71,353,218,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + LTEXT "",IDC_STATIC_DETAIL,442,325,181,58 +END + +IDD_DIALOG_ORIGINAL_AUDIO_ DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310,NOT WS_VISIBLE + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,328,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,71,326,218,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,307,326,50,14 + LTEXT "Caputre Video",IDC_STATIC_ORIGINAL_AUDIO,12,353,48,8 + COMBOBOX IDC_COMBO_ORIGINAL_AUDIO,71,353,218,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + LTEXT "",IDC_STATIC_DETAIL,442,325,181,58 + PUSHBUTTON "JoinChannel",IDC_BUTTON_SET_AUDIO_PROC,308,354,50,14 + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP +END + +IDD_DIALOG_MEDIA_PLAYER DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,328,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,70,326,218,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,307,326,60,14 + LTEXT "",IDC_STATIC_DETAIL,442,325,181,58 + LTEXT "video source",IDC_STATIC_VIDEO_SOURCE,11,347,48,8 + EDITTEXT IDC_EDIT_VIDEO_SOURCE,70,346,218,13,ES_AUTOHSCROLL + PUSHBUTTON "open",IDC_BUTTON_OPEN,306,346,60,14 + PUSHBUTTON "stop",IDC_BUTTON_STOP,9,363,60,14 + PUSHBUTTON "play",IDC_BUTTON_PLAY,76,363,60,14 + PUSHBUTTON "attch player",IDC_BUTTON_ATTACH,143,363,60,14 + PUSHBUTTON "publish video",IDC_BUTTON_PUBLISH_VIDEO,212,363,60,14 + PUSHBUTTON "publish audio",IDC_BUTTON_PUBLISH_AUDIO,280,363,60,14 + CONTROL "",IDC_SLIDER_VIDEO,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,10,377,355,15 +END + +IDD_DIALOG_VIDEO_PROFILE DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,328,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,71,326,228,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,307,326,60,14 + LTEXT "width",IDC_STATIC_VIDEO_WIDTH,11,352,48,8 + PUSHBUTTON "Set AudioProfile",IDC_BUTTON_SET_VIDEO_PROFILE,307,361,60,14 + LTEXT "",IDC_STATIC_DETAIL,442,325,181,58 + LTEXT "height",IDC_STATIC_VIDEO_HEIGHT,109,352,48,8 + EDITTEXT IDC_EDIT_VIDEO_WIDTH,43,350,58,13,ES_AUTOHSCROLL + EDITTEXT IDC_EDIT_VIDEO_HEIGHT,139,350,58,13,ES_AUTOHSCROLL + LTEXT "fps",IDC_STATIC_VIDEO_FPS,202,352,48,8 + LTEXT "bitrate",IDC_STATIC_VIDEO_BITRATE,11,368,48,8 + EDITTEXT IDC_EDIT_VIDEO_BITRATE,43,367,58,13,ES_AUTOHSCROLL + LTEXT "bitrate",IDC_STATIC_VIDEO_DEGRADATION_PREFERENCE,109,368,48,8 + COMBOBOX IDC_COMBO_DEGRADATION_PREFERENCE,163,367,91,30,CBS_DROPDOWN | CBS_SORT | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_COMBO_FPS,220,350,79,40,CBS_DROPDOWN | CBS_SORT | WS_VSCROLL | WS_TABSTOP +END + +IDD_DIALOG_MEDIA_ENCRYPT DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,328,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,71,326,218,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,307,326,50,14 + LTEXT "Caputre Video",IDC_STATIC_ENCRYPT_MODE,12,353,48,8 + PUSHBUTTON "Start Capture",IDC_BUTTON_SET_MEDIA_ENCRYPT,307,362,50,14 + COMBOBOX IDC_COMBO_ENCRYPT_MODE,71,353,218,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + LTEXT "",IDC_STATIC_DETAIL,442,325,181,58 + LTEXT "encrypt key",IDC_STATIC_ENCRYPT_KEY,12,374,48,8 + EDITTEXT IDC_EDIT_ENCRYPT_KEY,71,373,218,13,ES_AUTOHSCROLL +END + +IDD_DIALOG_CUSTOM_CAPTURE_MEDIA_IO_VIDEO DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,328,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,71,326,218,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,307,326,50,14 + LTEXT "Caputre Video",IDC_STATIC_CAPTUREDEVICE,12,353,48,8 + PUSHBUTTON "Start Capture",IDC_BUTTON_START_CAPUTRE,384,352,50,14 + COMBOBOX IDC_COMBO_CAPTURE_VIDEO_DEVICE,71,353,149,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + LTEXT "",IDC_STATIC_DETAIL,442,325,181,58 + COMBOBOX IDC_COMBO_CAPTURE_VIDEO_TYPE,225,353,149,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP +END + +IDD_DIALOG_AUDIO_EFFECT DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,328,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,71,326,218,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,307,326,60,14 + LTEXT "effect path",IDC_STATIC_AUDIO_EFFECT_PATH,11,346,48,8 + PUSHBUTTON "Add Effect",IDC_BUTTON_ADD_EFFECT,307,347,60,14 + LTEXT "",IDC_STATIC_DETAIL,508,313,122,16 + EDITTEXT IDC_EDIT_AUDIO_EFFECT_PATH,71,347,218,13,ES_AUTOHSCROLL + LTEXT "repeat times",IDC_STATIC_AUDIO_REPEAT,11,384,47,8 + EDITTEXT IDC_EDIT_AUDIO_REPEAT_TIMES,59,383,44,13,ES_AUTOHSCROLL + LTEXT "gain",IDC_STATIC_AUDIO_AGIN,109,384,18,8 + CONTROL "",IDC_SPIN_AGIN,"msctls_updown32",UDS_ARROWKEYS,152,382,10,13 + EDITTEXT IDC_EDIT_AUDIO_AGIN,126,383,26,13,ES_AUTOHSCROLL | ES_READONLY + LTEXT "pitch",IDC_STATIC_AUDIO_PITCH,166,384,18,8 + CONTROL "",IDC_SPIN_PITCH,"msctls_updown32",UDS_ARROWKEYS,209,382,10,14 + EDITTEXT IDC_EDIT_AUDIO_PITCH,183,383,26,14,ES_AUTOHSCROLL | ES_READONLY + COMBOBOX IDC_COMBO_PAN,240,383,34,30,CBS_DROPDOWN | CBS_SORT | WS_VSCROLL | WS_TABSTOP + CONTROL "publish",IDC_CHK_PUBLISH,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,281,385,38,10 + LTEXT "effect",IDC_STATIC_AUDIO_EFFECT,11,364,48,8 + COMBOBOX IDC_COMBO2,71,364,218,30,CBS_DROPDOWN | CBS_SORT | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "Remove Effect",IDC_BUTTON_REMOVE,307,364,60,14 + PUSHBUTTON "preload",IDC_BUTTON_PRELOAD,374,326,60,14 + PUSHBUTTON "play",IDC_BUTTON_PLAY_EFFECT,441,365,60,14 + PUSHBUTTON "Pause Effect",IDC_BUTTON_PAUSE_EFFECT,510,365,60,14 + PUSHBUTTON "Pause All Effect",IDC_BUTTON_PAUSE_ALL_EFFECT,374,347,60,14 + PUSHBUTTON "unPreload",IDC_BUTTON_UNLOAD_EFFECT,441,326,60,14 + PUSHBUTTON "Resume Effect",IDC_BUTTON_RESUME_EFFECT,510,347,60,14 + PUSHBUTTON "Stop All Effect",IDC_BUTTON_STOP_ALL_EFFECT2,441,347,60,14 + LTEXT "pan",IDC_STATIC_AUDIO_PAN,221,384,18,8 + PUSHBUTTON "Stop Effect",IDC_BUTTON_STOP_EFFECT,374,364,60,14 + CONTROL "",IDC_SLIDER_VLOUME,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,367,382,147,15 + LTEXT "vloume",IDC_STATIC_AUDIO_VLOUME,327,386,42,8 +END + +IDD_DIALOG_BEAUTY DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,328,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,71,326,218,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,307,326,50,14 + LTEXT "lightening contrast",IDC_STATIC_BEAUTY_LIGHTENING_CONTRAST_LEVEL,11,353,93,8 + COMBOBOX IDC_COMBO_BEAUTE_LIGHTENING_CONTRAST_LEVEL,80,352,79,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + LTEXT "",IDC_STATIC_DETAIL,442,325,181,58 + LTEXT "lightening",IDC_STATIC_BEAUTY_LIGHTENING,11,370,48,8 + EDITTEXT IDC_EDIT_LIGHTENING,79,369,80,13,ES_AUTOHSCROLL + LTEXT "redness",IDC_STATIC_BEAUTY_REDNESS,166,353,48,8 + LTEXT "smoothness",IDC_STATIC_BEAUTY_SMOOTHNESS,166,371,48,8 + EDITTEXT IDC_EDIT_BEAUTY_REDNESS,222,351,80,13,ES_AUTOHSCROLL + EDITTEXT IDC_EDIT_BEAUTY_SMOOTHNESS,222,370,80,13,ES_AUTOHSCROLL + CONTROL "Beauty Enable",IDC_CHECK_BEAUTY_ENABLE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,310,358,62,10 +END + +IDD_DIALOG_PERCALL_TEST DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,70,128,306,185,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "camera",IDC_STATIC_ADUIO_INPUT,64,24,48,8 + PUSHBUTTON "Set AudioProfile",IDC_BUTTON_AUDIO_INPUT_TEST,337,22,60,14 + COMBOBOX IDC_COMBO_AUDIO_INPUT,112,22,218,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + LTEXT "",IDC_STATIC_DETAIL,442,325,181,58 + LTEXT "Audio Output",IDC_STATIC_ADUIO_SCENARIO,64,58,48,8 + COMBOBOX IDC_COMBO_AUDIO_OUTPUT,112,57,218,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + LTEXT "volume",IDC_STATIC_ADUIO_INPUT_VOL,64,42,48,8 + LTEXT "volume",IDC_STATIC_ADUIO_OUTPUT_VOL,64,74,48,8 + CONTROL "",IDC_SLIDER_INPUT_VOL,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,106,39,226,15 + CONTROL "",IDC_SLIDER_OUTPUT_VOL,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,106,74,224,15 + PUSHBUTTON "Set AudioProfile",IDC_BUTTON_AUDIO_OUTPUT_TEST,337,60,60,14 + LTEXT "camera",IDC_STATIC_CAMERA,66,101,34,8 + COMBOBOX IDC_COMBO_VIDEO,110,99,218,30,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "Set AudioProfile",IDC_BUTTON_CAMERA,337,98,60,14 +END + +IDD_DIALOG_VOLUME DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,328,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,71,326,218,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,307,326,60,14 + LTEXT "Audio Change",IDC_STATIC_AUDIO_CAP_VOL,12,346,48,8 + LTEXT "",IDC_STATIC_DETAIL,489,325,134,58 + CONTROL "",IDC_SLIDER_CAP_VOLUME,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,67,343,123,15 + LTEXT "Audio Change",IDC_STATIC_AUDIO_SIGNAL_VOL,200,346,48,8 + CONTROL "",IDC_SLIDER_SIGNAL_VOLUME2,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,251,343,123,15 + LTEXT "Audio Change",IDC_STATIC_PLAYBACK_VOL,11,363,48,8 + LTEXT "Audio Change",IDC_STATIC_PLAYBACK_VOL_SIGNAL,200,363,48,8 + CONTROL "",IDC_SLIDER_PLAYBACK_SIGNAL_VOLUME,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,251,360,123,15 + CONTROL "",IDC_SLIDER_PLAYBACK_VOLUME,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,67,360,123,15 + LTEXT "Static",IDC_STATIC_SPEAKER_INFO,11,378,358,19 +END + +IDD_DIALOG_PEPORT_IN_CALL DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "Static",IDC_STATIC_BITRATE_ALL_VAL,71,376,76,8 + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,323,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,71,321,218,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,307,321,60,14 + LTEXT "",IDC_STATIC_DETAIL,492,325,131,58 + LTEXT "txBytes/rxBytes",IDC_STATIC_TXBYTES_RXBTYES,11,356,54,8 + LTEXT "txKBitRate/rxKBitRate",IDC_STATIC_BITRATE_ALL,10,376,48,8 + LTEXT "Static",IDC_STATIC_TXBYTES_RXBYTES_VAL,71,357,76,8 + LTEXT "Uplink/Downlink",IDC_STATIC_AUDIO_NETWORK_DELAY,147,359,60,8 + LTEXT "Static",IDC_STATIC_AUDIO_NETWORK_DELAY_VAL,207,359,85,8 + LTEXT "Uplink/Downlink",IDC_STATIC_AUDIO_RECIVED_BITRATE,147,376,60,8 + LTEXT "Static",IDC_STATIC_AUDIO_RECVIED_BITRATE_VAL,206,376,85,8 + LTEXT "Uplink/Downlink",IDC_STATIC_VIDEO_NETWORK_DELAY,303,358,60,8 + LTEXT "Static",IDC_STATIC_VEDIO_NETWORK_DELAY_VAL,365,359,85,8 + LTEXT "Uplink/Downlink",IDC_STATIC_VEDIO_RECIVED_BITRATE,303,376,60,8 + LTEXT "Static",IDC_STATIC_VEDIO_RECVIED_BITRATE_VAL2,364,376,85,8 + LTEXT "txKBitRate/rxKBitRate",IDC_STATIC_LOCAL_VIDEO_WIDTH_HEIGHT,145,340,45,8 + LTEXT "Static",IDC_STATIC_LOCAL_VIDEO_WITH_HEIGHT_VAL,196,340,76,8 + LTEXT "txKBitRate/rxKBitRate",IDC_STATIC_LOCAL_VIDEO_FPS,303,339,51,8 + GROUPBOX "Static",IDC_STATIC_VIDEO_REMOTE,300,348,152,44 + GROUPBOX "Static",IDC_STATIC_AUDIO_REMOTE,144,348,152,44 + GROUPBOX "Static",IDC_STATIC_NETWORK_TOTAL,4,336,131,58 + LTEXT "txKBitRate/rxKBitRate",IDC_STATIC_LOCAL_VIDEO_FPS_VAL,361,339,78,8 +END + +IDD_DIALOG_REGIONAL_CONNECTION DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,328,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,71,326,218,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,307,336,60,14 + LTEXT "Audio Change",IDC_STATIC_AREA_CODE,11,345,48,8 + LTEXT "",IDC_STATIC_DETAIL,442,325,181,58 + COMBOBOX IDC_COMBO_AREA_CODE,71,345,217,30,CBS_DROPDOWN | CBS_SORT | WS_VSCROLL | WS_TABSTOP +END + +IDD_DIALOG_CROSS_CHANNEL DIALOGEX 0, 0, 632, 400 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_STATIC_VIDEO,1,0,483,310,NOT WS_VISIBLE + LISTBOX IDC_LIST_INFO_BROADCASTING,491,0,139,312,LBS_NOINTEGRALHEIGHT | LBS_DISABLENOSCROLL | WS_VSCROLL | WS_HSCROLL | WS_TABSTOP + LTEXT "Channel Name",IDC_STATIC_CHANNELNAME,11,320,48,8 + EDITTEXT IDC_EDIT_CHANNELNAME,71,318,219,13,ES_AUTOHSCROLL + PUSHBUTTON "JoinChannel",IDC_BUTTON_JOINCHANNEL,295,318,60,14 + LTEXT "Audio Change",IDC_STATIC_CROSS_CHANNEL,11,333,48,8 + PUSHBUTTON "Set AudioChange",IDC_BUTTON_ADD_CROSS_CHANNEL,296,349,60,14 + LTEXT "",IDC_STATIC_DETAIL,487,325,136,58 + EDITTEXT IDC_EDIT_CROSS_CHANNEL,71,333,219,13,ES_AUTOHSCROLL + EDITTEXT IDC_EDIT_TOKEN,71,348,219,13,ES_AUTOHSCROLL + LTEXT "Audio Change",IDC_STATIC_TOKEN,11,349,48,8 + LTEXT "Audio Change",IDC_USER_ID,11,365,48,8 + EDITTEXT IDC_EDIT_USER_ID,71,364,219,13,ES_AUTOHSCROLL + LTEXT "Audio Change",IDC_CROSS_CHANNEL_LIST,11,384,48,8 + COMBOBOX IDC_COMBO_CROSS_CAHNNEL_LIST,71,381,219,30,CBS_DROPDOWN | CBS_SORT | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "Set AudioChange",IDC_BUTTON_REMOVE_CROSS_CHANNEL2,296,381,60,14 + PUSHBUTTON "Set AudioChange",IDC_BUTTON_START_MEDIA_RELAY,359,381,60,14 + PUSHBUTTON "Set AudioChange",IDC_BUTTON_UPDATE,421,381,60,14 +END + + +///////////////////////////////////////////////////////////////////////////// +// +// DESIGNINFO +// + +#ifdef APSTUDIO_INVOKED +GUIDELINES DESIGNINFO +BEGIN + IDD_DIALOG_LIVEBROADCASTING, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_RTMPINJECT, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_RTMP_STREAMING, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_METADATA, DIALOG + BEGIN + RIGHTMARGIN, 630 + VERTGUIDE, 466 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_SCREEN_SHARE, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_CUSTOM_CAPTURE_VIDEO, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_CUSTOM_CAPTURE_AUDIO, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_MULTI_CHANNEL, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_AUDIO_PROFILE, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_BEAUTY_AUDIO, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_AUDIO_MIX, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_ORIGINAL_VIDEO, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_CUSTOM_ENCRYPT, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_ORIGINAL_AUDIO_, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_MEDIA_PLAYER, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_VIDEO_PROFILE, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_MEDIA_ENCRYPT, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_CUSTOM_CAPTURE_MEDIA_IO_VIDEO, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_AUDIO_EFFECT, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_BEAUTY, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_PERCALL_TEST, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_VOLUME, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_PEPORT_IN_CALL, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_REGIONAL_CONNECTION, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END + + IDD_DIALOG_CROSS_CHANNEL, DIALOG + BEGIN + RIGHTMARGIN, 630 + BOTTOMMARGIN, 397 + END +END +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// AFX_DIALOG_LAYOUT +// + +IDD_DIALOG_LIVEBROADCASTING AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_RTMPINJECT AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_RTMP_STREAMING AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_METADATA AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_SCREEN_SHARE AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_CUSTOM_CAPTURE_VIDEO AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_CUSTOM_CAPTURE_AUDIO AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_MULTI_CHANNEL AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_AUDIO_PROFILE AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_BEAUTY_AUDIO AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_AUDIO_MIX AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_ORIGINAL_VIDEO AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_CUSTOM_ENCRYPT AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_ORIGINAL_AUDIO_ AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_MEDIA_PLAYER AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_VIDEO_PROFILE AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_MEDIA_ENCRYPT AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_CUSTOM_CAPTURE_MEDIA_IO_VIDEO AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_AUDIO_EFFECT AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_BEAUTY AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_PERCALL_TEST AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_VOLUME AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_PEPORT_IN_CALL AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_REGIONAL_CONNECTION AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_DIALOG_CROSS_CHANNEL AFX_DIALOG_LAYOUT +BEGIN + 0 +END + + +///////////////////////////////////////////////////////////////////////////// +// +// Bitmap +// + +IDB_BITMAP_NETWORK_STATE BITMAP "res\\IDB_NETWORK_QUALITY.bmp" + + +///////////////////////////////////////////////////////////////////////////// +// +// WAVE +// + +IDR_TEST_WAVE WAVE "res\\ID_TEST_AUDIO.wav" + +#endif // (壬й) resources +///////////////////////////////////////////////////////////////////////////// + + +///////////////////////////////////////////////////////////////////////////// +// Ӣ() resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog +// + +IDD_ABOUTBOX DIALOGEX 0, 0, 170, 62 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "About APIExample" +FONT 8, "MS Shell Dlg", 0, 0, 0x1 +BEGIN + ICON IDR_MAINFRAME,IDC_STATIC,14,14,21,20 + LTEXT "APIExample, Version 1.0",IDC_STATIC,42,14,114,8,SS_NOPREFIX + LTEXT "Copyright (C) 2020",IDC_STATIC,42,26,114,8 + DEFPUSHBUTTON "OK",IDOK,113,41,50,14,WS_GROUP +END + +IDD_APIEXAMPLE_DIALOG DIALOGEX 0, 0, 836, 435 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU +EXSTYLE WS_EX_OVERLAPPEDWINDOW | WS_EX_APPWINDOW +FONT 8, "MS Shell Dlg", 0, 0, 0x1 +BEGIN + PUSHBUTTON "Document Website",IDC_BUTTON_DOCUMENT_WEBSITE,1,15,172,24 + PUSHBUTTON "FAQ",IDC_BUTTON_FAQ,1,38,172,23 + PUSHBUTTON "??????",IDC_BUTTON_REGISTER,1,60,172,23 + PUSHBUTTON "Github",IDC_BUTTON_DEMO,1,83,172,23 + GROUPBOX "",IDC_STATIC_MAIN,187,5,639,422 + GROUPBOX "Document",IDC_STATIC_GROUP_DOC,1,5,174,106 + GROUPBOX "Basic Scene",IDC_STATIC_GROUP_LIST,2,140,176,286 + CONTROL "",IDC_LIST_BASIC,"SysTreeView32",TVS_SHOWSELALWAYS | TVS_TRACKSELECT | WS_BORDER | WS_HSCROLL | WS_GROUP | WS_TABSTOP,4,148,171,97 + CONTROL "",IDC_LIST_ADVANCED,"SysTreeView32",WS_BORDER | WS_HSCROLL | WS_GROUP | WS_TABSTOP,5,271,171,150 + LTEXT "Advanced Scene",IDC_STATIC_ADVANCE,11,258,83,8 + LTEXT "",IDC_STATIC_LIST_INFO,5,112,172,19 +END + + +///////////////////////////////////////////////////////////////////////////// +// +// DESIGNINFO +// + +#ifdef APSTUDIO_INVOKED +GUIDELINES DESIGNINFO +BEGIN + IDD_ABOUTBOX, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 163 + TOPMARGIN, 7 + BOTTOMMARGIN, 55 + END + + IDD_APIEXAMPLE_DIALOG, DIALOG + BEGIN + RIGHTMARGIN, 834 + BOTTOMMARGIN, 434 + END +END +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// AFX_DIALOG_LAYOUT +// + +IDD_APIEXAMPLE_DIALOG AFX_DIALOG_LAYOUT +BEGIN + 0 +END + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION 1,0,0,1 + PRODUCTVERSION 1,0,0,1 + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x40004L + FILETYPE 0x1L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "CompanyName", "TODO: " + VALUE "FileDescription", "APIExample" + VALUE "FileVersion", "1.0.0.1" + VALUE "InternalName", "APIExample.exe" + VALUE "LegalCopyright", "TODO: (c) . All rights reserved." + VALUE "OriginalFilename", "APIExample.exe" + VALUE "ProductName", "TODO: " + VALUE "ProductVersion", "1.0.0.1" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE +BEGIN + IDS_ABOUTBOX "&About APIExample..." +END + +#endif // Ӣ() resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// +#define _AFX_NO_SPLITTER_RESOURCES +#define _AFX_NO_OLE_RESOURCES +#define _AFX_NO_TRACKER_RESOURCES +#define _AFX_NO_PROPERTY_RESOURCES + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE 9, 1 +#include "res\APIExample.rc2" // non-Microsoft Visual C++ edited resources +#include "afxres.rc" // Standard components +#if !defined(_AFXDLL) +#include "afxribbon.rc" // MFC ribbon and control bar resources +#endif +#endif + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/windows/APIExample/APIExample/APIExample.vcxproj b/windows/APIExample/APIExample/APIExample.vcxproj new file mode 100644 index 000000000..0b98f454f --- /dev/null +++ b/windows/APIExample/APIExample/APIExample.vcxproj @@ -0,0 +1,387 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {DB16CA2F-3910-4449-A5BD-6A602B33BE0F} + MFCProj + APIExample + 8.1 + + + + Application + true + v141 + Unicode + Dynamic + + + Application + false + v141 + true + Unicode + Dynamic + + + Application + true + v141 + Unicode + Dynamic + + + Application + false + v141 + true + Unicode + Dynamic + + + + + + + + + + + + + + + + + + + + + true + true + + + true + + + false + + + false + + + + NotUsing + Level3 + Disabled + false + WIN32;_WINDOWS;_DEBUG;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions); + $(SolutionDir)libs\include;$(solutionDir)ThirdParty\libYUV;$(ProjectDir);$(ProjectDir)RtcChannelHelperPlugin;$(solutionDir)MediaPlayerPart\include + MultiThreadedDLL + + + Windows + $(SolutionDir)libs\x86;$(SolutionDir)ThirdParty\libyuv\debug;$(SolutionDir)ThirdParty\DShow;$(SolutionDir)MediaPlayerPart\lib + libcmt.lib + AgoraMediaPlayer.lib;d3d9.lib;dsound.lib;winmm.lib;dxguid.lib + + + false + true + _DEBUG;%(PreprocessorDefinitions) + + + 0x0409 + _DEBUG;%(PreprocessorDefinitions) + $(IntDir);%(AdditionalIncludeDirectories) + + + if exist $(SolutionDir)libs (copy $(SolutionDir)libs\x86\*.dll $(SolutionDir)$(Configuration)) +if exist zh-cn.ini (copy zh-cn.ini $(SolutionDir)$(Configuration)) +if exist en.ini (copy en.ini $(SolutionDir)$(Configuration)) +if exist $(SolutionDir)MediaPlayerPart (copy $(SolutionDir)MediaPlayerPart\dll\AgoraMediaPlayer.dll $(SolutionDir)$(Configuration)) + + + + + $(IntDir)$(TargetName)$(TargetExt).embed.manifest + PerMonitorHighDPIAware + + + $(IntDir)$(TargetName)$(TargetExt).embed.manifest.res + + + + + Use + Level3 + Disabled + true + _WINDOWS;_DEBUG;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions);_WIN32_WINNT + $(SolutionDir)\libs\include;..\libs\include; + MultiThreadedDLL + + + Windows + $(SolutionDir)\libs\x86_64 + libcmt.lib + + + false + true + _DEBUG;%(PreprocessorDefinitions) + + + 0x0409 + _DEBUG;%(PreprocessorDefinitions) + $(IntDir);%(AdditionalIncludeDirectories) + + + if exist $(SolutionDir)\libs (copy $(SolutionDir)libs\x86_64\agora_rtc_sdk.dll $(SolutionDir)$(Platform)\$(Configuration)) +if exist zh-cn.ini (copy zh-cn.ini $(SolutionDir)$(Platform)\$(Configuration)) +if exist en.ini (copy en.ini $(SolutionDir)$(Platform)\$(Configuration)) + + + + PerMonitorHighDPIAware + + + + + NotUsing + Level3 + MaxSpeed + true + true + false + WIN32;_WINDOWS;NDEBUG;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions); + $(SolutionDir)libs\include;$(solutionDir)ThirdParty\libYUV;$(ProjectDir);$(ProjectDir)RtcChannelHelperPlugin;$(solutionDir)MediaPlayerPart\include + MultiThreadedDLL + + + Windows + true + true + $(SolutionDir)libs\x86;$(SolutionDir)ThirdParty\libyuv\release;$(SolutionDir)ThirdParty\DShow;$(SolutionDir)MediaPlayerPart\lib + libcmt.lib + AgoraMediaPlayer.lib;d3d9.lib;dsound.lib;winmm.lib;dxguid.lib + + + false + true + NDEBUG;%(PreprocessorDefinitions) + + + 0x0409 + NDEBUG;%(PreprocessorDefinitions) + $(IntDir);%(AdditionalIncludeDirectories) + + + if exist $(SolutionDir)libs (copy $(SolutionDir)libs\x86\*.dll $(SolutionDir)$(Configuration)) +if exist zh-cn.ini (copy zh-cn.ini $(SolutionDir)$(Configuration)) +if exist en.ini (copy en.ini $(SolutionDir)$(Configuration)) +if exist $(SolutionDir)MediaPlayerPart (copy $(SolutionDir)MediaPlayerPart\dll\AgoraMediaPlayer.dll $(SolutionDir)$(Configuration)) + + + + + + + PerMonitorHighDPIAware + + + + + Use + Level3 + MaxSpeed + true + true + true + _WINDOWS;NDEBUG;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions);_WIN32_WINNT + $(SolutionDir)libs\include + MultiThreadedDLL + + + Windows + true + true + $(SolutionDir)\libs\x86_64 + libcmt.lib + + + false + true + NDEBUG;%(PreprocessorDefinitions) + + + 0x0409 + NDEBUG;%(PreprocessorDefinitions) + $(IntDir);%(AdditionalIncludeDirectories) + + + if exist $(SolutionDir)\libs (copy $(SolutionDir)libs\x86_64\agora_rtc_sdk.dll $(SolutionDir)$(Platform)\$(Configuration)) +if exist zh-cn.ini (copy zh-cn.ini $(SolutionDir)$(Platform)\$(Configuration)) +if exist en.ini (copy en.ini $(SolutionDir)$(Platform)\$(Configuration)) + + + + PerMonitorHighDPIAware + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NotUsing + + + NotUsing + + + NotUsing + + + NotUsing + + + NotUsing + + + NotUsing + + + + + + + Create + Create + Create + Create + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/windows/APIExample/APIExample/APIExample.vcxproj.filters b/windows/APIExample/APIExample/APIExample.vcxproj.filters new file mode 100644 index 000000000..d1d968602 --- /dev/null +++ b/windows/APIExample/APIExample/APIExample.vcxproj.filters @@ -0,0 +1,422 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {7e4aa55f-ad9a-4a32-ac0d-5017d6884205} + + + {fd0133e8-4ed1-4da5-9eb0-2eed02844959} + + + {0637d784-14a1-4260-901a-caa73030f229} + + + {ed782797-3b06-44a9-8894-6b9d93d0dfea} + + + {7682a8b6-0c3d-46bc-8519-8ec74432ff48} + + + {8527d597-a232-4485-beae-d1aa2190e5e0} + + + {6b8d2d7b-3c5b-46b6-828b-57d6f6aa2ba1} + + + {09920163-bafa-4443-925e-093feb659403} + + + {d9c87de4-1367-48c3-ac3f-6ed2275c781b} + + + {e95f9d7e-35e4-4ad9-beb1-d4afebdd4b00} + + + {1e6dafd7-4b82-41a4-897f-3cf3d81a5a70} + + + {0d6b76b2-1183-432c-88e6-cf2fee5ac7b8} + + + {1008b46e-9830-4fb5-a3a3-cddbc96d8a48} + + + {dfe1f155-efc1-492e-8ecf-35d55d7bf53c} + + + {219030b8-448b-46a3-b252-f70de65d1ec5} + + + {7c921065-1d3e-4f12-8d92-5770ca80d3bd} + + + {c39d4662-1f18-4664-bcc6-d92fa51d6af5} + + + {c4034334-7c64-4bd9-8952-a453a8220c35} + + + {caf2a8e2-4483-4f70-a997-1bde44f82cf4} + + + {872801c8-5652-4c02-8fd6-640c1303f89a} + + + {18db26ed-a1e3-4433-8fe9-b7c1aad1c767} + + + {6e290d1d-4cb6-4ceb-83fb-f2a1f5dfc712} + + + {5beb4aaf-15b7-4839-92e9-697b1cd15636} + + + {c384154e-d846-404d-a20c-48ff01c2c11a} + + + {e1683efe-8fbe-45b1-a676-ffecc6a0aeb0} + + + {4ea083f3-3c9b-4831-a8ea-b495e9a7b26f} + + + {04bba177-86b2-4d30-8dc2-b4470312c0c9} + + + {1ef7530c-29ff-4882-a357-fcb3d5a14d04} + + + {0e35302b-204e-4cd0-81c2-882dd3b99eca} + + + {5674a9ae-823a-41eb-9101-c82700ecbfe4} + + + {cf51ce8e-f1ab-472d-b82f-e64b2492abb8} + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Advanced\CustomAudioCapture + + + Advanced\CustomVideoCapture + + + Advanced\RTMPInject + + + Advanced\RTMPStream + + + Advanced\ScreenShare + + + Advanced\VideoMetadata + + + Basic\LiveBroadcasting + + + DirectShow + + + DirectShow + + + DirectShow + + + DirectShow + + + DirectShow + + + DirectShow + + + DirectShow + + + Advanced\AudioProfile + + + Advanced\BeautyAudio + + + Advanced\AudioMixing + + + Advanced\OriginalVideo + + + Advanced\OriginalAudio + + + Advanced\CustomEncrypt + + + MeidaPlayer + + + MeidaPlayer + + + MeidaPlayer + + + MeidaPlayer + + + MeidaPlayer + + + MeidaPlayer + + + MeidaPlayer + + + d3d + + + Advanced\VideoProfile + + + Advanced\MediaEncrypt + + + Advanced\MediaPlayer + + + Advanced\MediaIOCustomVideoCapture + + + dsound + + + Advanced\AudioEffect + + + Advanced\Beauty + + + Advanced\MultiChannel + + + Header Files + + + Advanced\AudioVolume + + + Advanced\ReportInCall + + + Advanced\RegionConn + + + Advanced\CrossChannel + + + Advanced\PreCallTest + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Advanced\CustomAudioCapture + + + Advanced\CustomVideoCapture + + + Advanced\RTMPInject + + + Advanced\RTMPStream + + + Advanced\ScreenShare + + + Advanced\VideoMetadata + + + Basic\LiveBroadcasting + + + DirectShow + + + DirectShow + + + DirectShow + + + DirectShow + + + DirectShow + + + DirectShow + + + Advanced\AudioProfile + + + Advanced\BeautyAudio + + + Advanced\AudioMixing + + + Advanced\OriginalVideo + + + Advanced\OriginalAudio + + + Advanced\CustomEncrypt + + + MeidaPlayer + + + MeidaPlayer + + + MeidaPlayer + + + d3d + + + Advanced\VideoProfile + + + Advanced\MediaEncrypt + + + Advanced\MediaPlayer + + + Advanced\MediaIOCustomVideoCapture + + + Advanced\AudioEffect + + + Advanced\Beauty + + + Advanced\MultiChannel + + + Source Files + + + Advanced\AudioVolume + + + Advanced\ReportInCall + + + Advanced\RegionConn + + + Advanced\CrossChannel + + + Advanced\PreCallTest + + + dsound + + + + + Resource Files + + + + + Resource Files + + + + + Resource Files + + + Resource Files + + + Resource Files + + + + + Resource Files + + + \ No newline at end of file diff --git a/windows/APIExample/APIExample/APIExampleDlg.cpp b/windows/APIExample/APIExample/APIExampleDlg.cpp new file mode 100644 index 000000000..d4d806ee1 --- /dev/null +++ b/windows/APIExample/APIExample/APIExampleDlg.cpp @@ -0,0 +1,671 @@ + +// APIExampleDlg.cpp : implementation file +// + +#include "stdafx.h" +#include "APIExample.h" +#include "APIExampleDlg.h" + +#ifdef _DEBUG +#define new DEBUG_NEW +#endif + + +// CAboutDlg dialog used for App About + +class CAboutDlg : public CDialogEx +{ +public: + CAboutDlg(); + +// Dialog Data +#ifdef AFX_DESIGN_TIME + enum { IDD = IDD_ABOUTBOX }; +#endif + + protected: + virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support + +// Implementation +protected: + DECLARE_MESSAGE_MAP() +}; + +CAboutDlg::CAboutDlg() : CDialogEx(IDD_ABOUTBOX) +{ +} + +void CAboutDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); +} + +BEGIN_MESSAGE_MAP(CAboutDlg, CDialogEx) +END_MESSAGE_MAP() + + +// CAPIExampleDlg dialog + + + +CAPIExampleDlg::CAPIExampleDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_APIEXAMPLE_DIALOG, pParent) +{ + m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME); +} + +void CAPIExampleDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATIC_MAIN, m_staMainArea); + DDX_Control(pDX, IDC_LIST_BASIC, m_lstBasicScene); + DDX_Control(pDX, IDC_LIST_ADVANCED, m_lstAdvanced); + DDX_Control(pDX, IDC_STATIC_LIST_INFO, m_stalstInfo); + DDX_Control(pDX, IDC_STATIC_ADVANCE, m_staAdvancedScene); + DDX_Control(pDX, IDC_STATIC_GROUP_LIST, m_grpBasicScene); + DDX_Control(pDX, IDC_BUTTON_DEMO, m_btnDemoWebSite); + DDX_Control(pDX, IDC_BUTTON_REGISTER, m_btnRegister); + DDX_Control(pDX, IDC_BUTTON_FAQ, m_btnFAQ); + DDX_Control(pDX, IDC_BUTTON_DOCUMENT_WEBSITE, m_btnDocWebsite); + DDX_Control(pDX, IDC_STATIC_GROUP_DOC, m_grpDoc); +} + +BEGIN_MESSAGE_MAP(CAPIExampleDlg, CDialogEx) + ON_WM_SYSCOMMAND() + ON_WM_PAINT() + ON_WM_QUERYDRAGICON() + ON_BN_CLICKED(IDC_BUTTON_FAQ, &CAPIExampleDlg::OnBnClickedButtonFaq) + ON_BN_CLICKED(IDC_BUTTON_REGISTER, &CAPIExampleDlg::OnBnClickedButtonRegister) + ON_BN_CLICKED(IDC_BUTTON_DEMO, &CAPIExampleDlg::OnBnClickedButtonDemo) + + ON_NOTIFY(NM_CLICK, IDC_LIST_BASIC, &CAPIExampleDlg::OnClickListBasic) + ON_NOTIFY(NM_CLICK, IDC_LIST_ADVANCED, &CAPIExampleDlg::OnClickListAdvanced) + ON_NOTIFY(TVN_SELCHANGED, IDC_LIST_ADVANCED, &CAPIExampleDlg::OnSelchangedListAdvanced) + ON_NOTIFY(TVN_SELCHANGED, IDC_LIST_BASIC, &CAPIExampleDlg::OnSelchangedListBasic) + ON_NOTIFY(TVN_SELCHANGING, IDC_LIST_BASIC, &CAPIExampleDlg::OnSelchangingListBasic) + //ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAPIExampleDlg::OnEIDJoinLeaveChannel) + ON_NOTIFY(TVN_SELCHANGING, IDC_LIST_ADVANCED, &CAPIExampleDlg::OnSelchangingListAdvanced) + ON_BN_CLICKED(IDC_BUTTON_DOCUMENT_WEBSITE, &CAPIExampleDlg::OnBnClickedButtonDocumentWebsite) +END_MESSAGE_MAP() + + +// CAPIExampleDlg message handlers + +BOOL CAPIExampleDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + + // Add "About..." menu item to system menu. + + // IDM_ABOUTBOX must be in the system command range. + ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX); + ASSERT(IDM_ABOUTBOX < 0xF000); + + CMenu* pSysMenu = GetSystemMenu(FALSE); + if (pSysMenu != nullptr) + { + BOOL bNameValid; + CString strAboutMenu; + bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX); + ASSERT(bNameValid); + if (!strAboutMenu.IsEmpty()) + { + pSysMenu->AppendMenu(MF_SEPARATOR); + pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu); + } + } + + // Set the icon for this dialog. The framework does this automatically + // when the application's main window is not a dialog + SetIcon(m_hIcon, TRUE); // Set big icon + SetIcon(m_hIcon, FALSE); // Set small icon + + // TODO: Add extra initialization here + std::string strAppID = GET_APP_ID; + if (strcmp(strAppID.c_str(), "") == 0) + { + AfxMessageBox(_T("APP ID is not set, you can see readme file on how to get an APP ID.")); + ExitProcess(1); + } + InitCtrlText(); + InitSceneDialog(); + InitSceneList(); + + return TRUE; // return TRUE unless you set the focus to a control +} + +void CAPIExampleDlg::OnSysCommand(UINT nID, LPARAM lParam) +{ + if ((nID & 0xFFF0) == IDM_ABOUTBOX) + { + CAboutDlg dlgAbout; + dlgAbout.DoModal(); + } + else + { + CDialogEx::OnSysCommand(nID, lParam); + } +} + +// If you add a minimize button to your dialog, you will need the code below +// to draw the icon. For MFC applications using the document/view model, +// this is automatically done for you by the framework. + +void CAPIExampleDlg::OnPaint() +{ + if (IsIconic()) + { + CPaintDC dc(this); // device context for painting + + SendMessage(WM_ICONERASEBKGND, reinterpret_cast(dc.GetSafeHdc()), 0); + + // Center icon in client rectangle + int cxIcon = GetSystemMetrics(SM_CXICON); + int cyIcon = GetSystemMetrics(SM_CYICON); + CRect rect; + GetClientRect(&rect); + int x = (rect.Width() - cxIcon + 1) / 2; + int y = (rect.Height() - cyIcon + 1) / 2; + + // Draw the icon + dc.DrawIcon(x, y, m_hIcon); + } + else + { + CDialogEx::OnPaint(); + } +} + +// The system calls this function to obtain the cursor to display while the user drags +// the minimized window. +HCURSOR CAPIExampleDlg::OnQueryDragIcon() +{ + return static_cast(m_hIcon); +} + +void CAPIExampleDlg::InitCtrlText() +{ + m_staAdvancedScene.SetWindowText(commonAdvanceScene); + m_grpDoc.SetWindowText(commonGroupDoc); + m_grpBasicScene.SetWindowText(commonBasicScene); + m_btnFAQ.SetWindowText(commonFAQWebsite); + m_btnRegister.SetWindowText(commonRegisterWebsite); + m_btnDemoWebSite.SetWindowText(commonDemoWebsite); + m_btnDocWebsite.SetWindowText(commonDocumentWebsite); +} + +void CAPIExampleDlg::InitSceneDialog() +{ + //basic list + m_vecBasic.push_back(basicLiveBroadcasting); + + m_pLiveBroadcasting = new CLiveBroadcastingDlg(&m_staMainArea); + RECT rcArea, rcWnd; + m_staMainArea.GetWindowRect(&rcArea); + + m_pLiveBroadcasting->Create(CLiveBroadcastingDlg::IDD); + m_pLiveBroadcasting->GetWindowRect(&rcWnd); + int w = rcWnd.right - rcWnd.left; + int h = rcWnd.bottom - rcWnd.top; + rcWnd = { rcArea.left, rcArea.top - MAIN_AREA_TOP, rcArea.left + w, rcArea.top + h}; + m_pLiveBroadcasting->MoveWindow(&rcWnd); + + //advanced list + m_vecAdvanced.push_back(advancedRtmpInject); + m_vecAdvanced.push_back(advancedRtmpStreaming); + m_vecAdvanced.push_back(advancedVideoMetadata); + m_vecAdvanced.push_back(advancedVideoProfile); + m_vecAdvanced.push_back(advancedScreenCap); + m_vecAdvanced.push_back(advancedBeauty); + m_vecAdvanced.push_back(advancedBeautyAudio); + m_vecAdvanced.push_back(advancedAudioVolume); + m_vecAdvanced.push_back(advancedAudioProfile); + m_vecAdvanced.push_back(advancedAudioMixing); + m_vecAdvanced.push_back(advancedAudioEffect); + m_vecAdvanced.push_back(advancedCustomVideoCapture); + m_vecAdvanced.push_back(advancedMediaIOCustomVideoCapture); + m_vecAdvanced.push_back(advancedOriginalVideo); + m_vecAdvanced.push_back(advancedCustomAudioCapture); + m_vecAdvanced.push_back(advancedOriginalAudio); + m_vecAdvanced.push_back(advancedMediaEncrypt); + m_vecAdvanced.push_back(advancedCustomEncrypt); + m_vecAdvanced.push_back(advancedMediaPlayer); + m_vecAdvanced.push_back(advancedMultiChannel); + m_vecAdvanced.push_back(advancedPerCallTest); + m_vecAdvanced.push_back(advancedReportInCall); + m_vecAdvanced.push_back(advancedRegionConn); + m_vecAdvanced.push_back(advancedCrossChannel); + + + //inject + m_pRtmpInjectDlg = new CAgoraRtmpInjectionDlg(&m_staMainArea); + m_pRtmpInjectDlg->Create(CAgoraRtmpInjectionDlg::IDD); + m_pRtmpInjectDlg->MoveWindow(&rcWnd); + //rtmp + m_pRtmpStreamingDlg = new CAgoraRtmpStreamingDlg(&m_staMainArea); + m_pRtmpStreamingDlg->Create(CAgoraRtmpStreamingDlg::IDD); + m_pRtmpStreamingDlg->MoveWindow(&rcWnd); + + //video sei + m_pVideoSEIDlg = new CAgoraMetaDataDlg(&m_staMainArea); + m_pVideoSEIDlg->Create(CAgoraMetaDataDlg::IDD); + m_pVideoSEIDlg->MoveWindow(&rcWnd); + + //screen share + m_pScreenCap = new CAgoraScreenCapture(&m_staMainArea); + m_pScreenCap->Create(CAgoraScreenCapture::IDD); + m_pScreenCap->MoveWindow(&rcWnd); + + //beauty + m_pBeautyDlg = new CAgoraBeautyDlg(&m_staMainArea); + m_pBeautyDlg->Create(CAgoraBeautyDlg::IDD); + m_pBeautyDlg->MoveWindow(&rcWnd); + + //beauty audio + m_pBeautyAudio = new CAgoraBeautyAudio(&m_staMainArea); + m_pBeautyAudio->Create(CAgoraBeautyAudio::IDD); + m_pBeautyAudio->MoveWindow(&rcWnd); + + //video profile + m_pVideoProfileDlg = new CAgoraVideoProfileDlg(&m_staMainArea); + m_pVideoProfileDlg->Create(CAgoraVideoProfileDlg::IDD); + m_pVideoProfileDlg->MoveWindow(&rcWnd); + + + //audio profile + m_pAudioProfileDlg = new CAgoraAudioProfile(&m_staMainArea); + m_pAudioProfileDlg->Create(CAgoraAudioProfile::IDD); + m_pAudioProfileDlg->MoveWindow(&rcWnd); + + //audio mixing + m_pAudioMixingDlg = new CAgoraAudioMixingDlg(&m_staMainArea); + m_pAudioMixingDlg->Create(CAgoraAudioMixingDlg::IDD); + m_pAudioMixingDlg->MoveWindow(&rcWnd); + + //audio effect + m_pAudioEffectDlg = new CAgoraEffectDlg(&m_staMainArea); + m_pAudioEffectDlg->Create(CAgoraEffectDlg::IDD); + m_pAudioEffectDlg->MoveWindow(&rcWnd); + + //custom video capture + m_pCaputreVideoDlg = new CAgoraCaptureVideoDlg(&m_staMainArea); + m_pCaputreVideoDlg->Create(CAgoraCaptureVideoDlg::IDD); + m_pCaputreVideoDlg->MoveWindow(&rcWnd); + + //media io video capture + m_pMediaIOVideoDlg = new CAgoraMediaIOVideoCaptureDlg(&m_staMainArea); + m_pMediaIOVideoDlg->Create(CAgoraMediaIOVideoCaptureDlg::IDD); + m_pMediaIOVideoDlg->MoveWindow(&rcWnd); + + //original video process + m_pOriginalVideoDlg = new CAgoraOriginalVideoDlg(&m_staMainArea); + m_pOriginalVideoDlg->Create(CAgoraOriginalVideoDlg::IDD); + m_pOriginalVideoDlg->MoveWindow(&rcWnd); + + + //custom audio capture + m_pCaptureAudioDlg = new CAgoraCaptureAduioDlg(&m_staMainArea); + m_pCaptureAudioDlg->Create(CAgoraCaptureAduioDlg::IDD); + m_pCaptureAudioDlg->MoveWindow(&rcWnd); + + //original video process + m_pOriginalAudioDlg = new CAgoraOriginalAudioDlg(&m_staMainArea); + m_pOriginalAudioDlg->Create(CAgoraOriginalAudioDlg::IDD); + m_pOriginalAudioDlg->MoveWindow(&rcWnd); + + //media encrypt + m_pMediaEncryptDlg = new CAgoraMediaEncryptDlg(&m_staMainArea); + m_pMediaEncryptDlg->Create(CAgoraMediaEncryptDlg::IDD); + m_pMediaEncryptDlg->MoveWindow(&rcWnd); + + //custom encrypt + m_pCustomEncryptDlg = new CAgoraCustomEncryptDlg(&m_staMainArea); + m_pCustomEncryptDlg->Create(CAgoraCustomEncryptDlg::IDD); + m_pCustomEncryptDlg->MoveWindow(&rcWnd); + + //media player + m_pmediaPlayerDlg = new CAgoraMediaPlayer(&m_staMainArea); + m_pmediaPlayerDlg->Create(CAgoraMediaPlayer::IDD); + m_pmediaPlayerDlg->MoveWindow(&rcWnd); + + //multi channel + m_pMultiChannelDlg = new CAgoraMultiChannelDlg(&m_staMainArea); + m_pMultiChannelDlg->Create(CAgoraMultiChannelDlg::IDD); + m_pMultiChannelDlg->MoveWindow(&rcWnd); + + //per call test + m_pPerCallTestDlg = new CAgoraPreCallTestDlg(&m_staMainArea); + m_pPerCallTestDlg->Create(CAgoraPreCallTestDlg::IDD); + m_pPerCallTestDlg->MoveWindow(&rcWnd); + + //audio volume + m_pAudioVolumeDlg = new CAgoraAudioVolumeDlg(&m_staMainArea); + m_pAudioVolumeDlg->Create(CAgoraAudioVolumeDlg::IDD); + m_pAudioVolumeDlg->MoveWindow(&rcWnd); + + //report in call + m_pReportInCallDlg = new CAgoraReportInCallDlg(&m_staMainArea); + m_pReportInCallDlg->Create(CAgoraReportInCallDlg::IDD); + m_pReportInCallDlg->MoveWindow(&rcWnd); + + //Region Conn + m_pRegionConnDlg = new CAgoraRegionConnDlg(&m_staMainArea); + m_pRegionConnDlg->Create(CAgoraRegionConnDlg::IDD); + m_pRegionConnDlg->MoveWindow(&rcWnd); + + //cross channel + m_pCrossChannelDlg = new CAgoraCrossChannelDlg(&m_staMainArea); + m_pCrossChannelDlg->Create(CAgoraCrossChannelDlg::IDD); + m_pCrossChannelDlg->MoveWindow(&rcWnd); + +} + +void CAPIExampleDlg::InitSceneList() +{ + for (size_t i = 0; i < m_vecBasic.size(); i++){ + TVINSERTSTRUCT tvInsert; + tvInsert.hParent = NULL; + tvInsert.hInsertAfter = NULL; + tvInsert.item.mask = TVIF_TEXT; + tvInsert.item.pszText = m_vecBasic[i].GetBuffer(0); + m_lstBasicScene.InsertItem(&tvInsert); + } + + for (size_t i = 0; i < m_vecAdvanced.size(); i++) { + TVINSERTSTRUCT tvInsert; + tvInsert.hParent = NULL; + tvInsert.hInsertAfter = NULL; + tvInsert.item.mask = TVIF_TEXT; + tvInsert.item.pszText = m_vecAdvanced[i].GetBuffer(0); + m_lstAdvanced.InsertItem(&tvInsert); + } +} + + +void CAPIExampleDlg::OnBnClickedButtonFaq() +{ + ShellExecute(NULL, L"open", L"https://docs.agora.io/cn/Video/faq", NULL, NULL, SW_SHOWNORMAL); +} + + +void CAPIExampleDlg::OnBnClickedButtonRegister() +{ + ShellExecute(NULL, L"open", L"https://sso.agora.io/en/v2/signup", NULL, NULL, SW_SHOWNORMAL); +} + + +void CAPIExampleDlg::OnBnClickedButtonDemo() +{ + ShellExecute(NULL, L"open", L"https://github.com/AgoraIO/API-Examples", NULL, NULL, SW_SHOWNORMAL); +} + +void CAPIExampleDlg::OnSelchangedListBasic(NMHDR *pNMHDR, LRESULT *pResult) +{ + LPNMTREEVIEW pNMTreeView = reinterpret_cast(pNMHDR); + // TODO: Add your control notification handler code here + *pResult = 0; + + HTREEITEM hItem = m_lstBasicScene.GetSelectedItem(); + m_preSelectedItemText = m_lstBasicScene.GetItemText(hItem); + CreateScene(m_lstBasicScene, m_preSelectedItemText); +} + +void CAPIExampleDlg::OnSelchangedListAdvanced(NMHDR *pNMHDR, LRESULT *pResult) +{ + LPNMTREEVIEW pNMTreeView = reinterpret_cast(pNMHDR); + *pResult = 0; + HTREEITEM hItem = m_lstAdvanced.GetSelectedItem(); + m_preSelectedItemText = m_lstAdvanced.GetItemText(hItem); + CreateScene(m_lstBasicScene, m_preSelectedItemText); +} + +HTREEITEM CAPIExampleDlg::GetHitItem(NMHDR *pNMHDR) +{ + DWORD dwPos = GetMessagePos(); + POINT pt; + pt.x = LOWORD(dwPos); + pt.y = HIWORD(dwPos); + ::ScreenToClient(pNMHDR->hwndFrom, &pt); + TVHITTESTINFO ht = { 0 }; + + ht.flags = TVHT_ONITEM; + HTREEITEM hitItem = TreeView_HitTest(pNMHDR->hwndFrom, &ht); + + return hitItem ; +} + +void CAPIExampleDlg::OnClickListBasic(NMHDR *pNMHDR, LRESULT *pResult) +{ + *pResult = 0; +} + +void CAPIExampleDlg::OnClickListAdvanced(NMHDR *pNMHDR, LRESULT *pResult) +{ + *pResult = 0; + +} + +void CAPIExampleDlg::OnSelchangingListBasic(NMHDR *pNMHDR, LRESULT *pResult) +{ + LPNMTREEVIEW pNMTreeView = reinterpret_cast(pNMHDR); + HTREEITEM hOldItem = pNMTreeView->itemOld.hItem; + HTREEITEM hAdvancedItem = m_lstAdvanced.GetSelectedItem(); + + if (m_preSelectedItemText.Compare(m_lstBasicScene.GetItemText(hOldItem)) == 0) { + ReleaseScene(m_lstBasicScene, hOldItem); + } + else if (m_preSelectedItemText.Compare(m_lstAdvanced.GetItemText(hAdvancedItem)) == 0) { + // m_lstAdvanced.SetItemState(hAdvancedItem, 0, TVIS_SELECTED); + m_lstAdvanced.SelectItem(NULL); + ReleaseScene(m_lstAdvanced, hAdvancedItem); + } + + HTREEITEM hNewItem = pNMTreeView->itemNew.hItem; + *pResult = 0; +} + +void CAPIExampleDlg::OnSelchangingListAdvanced(NMHDR *pNMHDR, LRESULT *pResult) +{ + LPNMTREEVIEW pNMTreeView = reinterpret_cast(pNMHDR); + HTREEITEM hBasicItem = m_lstBasicScene.GetSelectedItem(); + HTREEITEM hOldItem = pNMTreeView->itemOld.hItem; + if (m_preSelectedItemText.Compare(m_lstBasicScene.GetItemText(hBasicItem)) == 0) { + m_lstBasicScene.SelectItem(NULL); + ReleaseScene(m_lstBasicScene, hBasicItem); + } + else if (m_preSelectedItemText.Compare(m_lstAdvanced.GetItemText(hOldItem)) == 0) { + ReleaseScene(m_lstAdvanced, hOldItem); + } + + *pResult = 0; +} + +// +void CAPIExampleDlg::CreateScene(CTreeCtrl& treeScene, CString selectedText) +{ + if (selectedText.Compare(basicLiveBroadcasting) == 0) { + m_pLiveBroadcasting->InitAgora(); + m_pLiveBroadcasting->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedRtmpInject) == 0) { + m_pRtmpInjectDlg->InitAgora(); + m_pRtmpInjectDlg->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedRtmpStreaming) == 0) { + m_pRtmpStreamingDlg->InitAgora(); + m_pRtmpStreamingDlg->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedVideoMetadata) == 0) { + m_pVideoSEIDlg->InitAgora(); + m_pVideoSEIDlg->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedScreenCap) == 0) { + m_pScreenCap->InitAgora(); + m_pScreenCap->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedCustomVideoCapture)==0) { + m_pCaputreVideoDlg->InitAgora(); + m_pCaputreVideoDlg->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedCustomAudioCapture)==0) { + m_pCaptureAudioDlg->InitAgora(); + m_pCaptureAudioDlg->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedBeauty) == 0) { + m_pBeautyDlg->InitAgora(); + m_pBeautyDlg->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedAudioProfile) == 0) { + m_pAudioProfileDlg->InitAgora(); + m_pAudioProfileDlg->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedBeautyAudio) == 0) { + m_pBeautyAudio->InitAgora(); + m_pBeautyAudio->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedAudioMixing) == 0) { + m_pAudioMixingDlg->InitAgora(); + m_pAudioMixingDlg->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedOriginalVideo) == 0) { + m_pOriginalVideoDlg->InitAgora(); + m_pOriginalVideoDlg->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedOriginalAudio) == 0) { + m_pOriginalAudioDlg->InitAgora(); + m_pOriginalAudioDlg->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedCustomEncrypt) == 0) { + m_pCustomEncryptDlg->InitAgora(); + m_pCustomEncryptDlg->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedMediaPlayer) == 0) { + m_pmediaPlayerDlg->InitAgora(); + m_pmediaPlayerDlg->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedVideoProfile) == 0){ + m_pVideoProfileDlg->InitAgora(); + m_pVideoProfileDlg->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedMediaEncrypt) == 0) { + m_pMediaEncryptDlg->InitAgora(); + m_pMediaEncryptDlg->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedMediaIOCustomVideoCapture) == 0) { + m_pMediaIOVideoDlg->InitAgora(); + m_pMediaIOVideoDlg->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedAudioEffect) == 0) { + m_pAudioEffectDlg->InitAgora(); + m_pAudioEffectDlg->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedMultiChannel) == 0) { + m_pMultiChannelDlg->InitAgora(); + m_pMultiChannelDlg->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedPerCallTest) == 0) { + m_pPerCallTestDlg->InitAgora(); + m_pPerCallTestDlg->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedAudioVolume) == 0) { + m_pAudioVolumeDlg->InitAgora(); + m_pAudioVolumeDlg->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedReportInCall) == 0) { + m_pReportInCallDlg->InitAgora(); + m_pReportInCallDlg->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedRegionConn) == 0) { + m_pRegionConnDlg->ShowWindow(SW_SHOW); + }else if (selectedText.Compare(advancedCrossChannel) == 0) { + m_pCrossChannelDlg->InitAgora(); + m_pCrossChannelDlg->ShowWindow(SW_SHOW); + } +} + +void CAPIExampleDlg::ReleaseScene(CTreeCtrl& treeScene, HTREEITEM& hSelectItem) +{ + CString str = treeScene.GetItemText(hSelectItem); + if (str.Compare(basicLiveBroadcasting) == 0 + && m_pLiveBroadcasting->IsWindowVisible()) {//pre sel release first + m_pLiveBroadcasting->UnInitAgora(); + m_pLiveBroadcasting->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedRtmpInject) == 0) { + m_pRtmpInjectDlg->UnInitAgora(); + m_pRtmpInjectDlg->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedRtmpStreaming) == 0) { + m_pRtmpStreamingDlg->UnInitAgora(); + m_pRtmpStreamingDlg->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedVideoMetadata) == 0) { + m_pVideoSEIDlg->UnInitAgora(); + m_pVideoSEIDlg->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedScreenCap) == 0){ + m_pScreenCap->UnInitAgora(); + m_pScreenCap->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedCustomVideoCapture) == 0) { + m_pCaputreVideoDlg->UnInitAgora(); + m_pCaputreVideoDlg->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedCustomAudioCapture) == 0) { + m_pCaptureAudioDlg->UnInitAgora(); + m_pCaptureAudioDlg->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedBeauty) == 0) { + m_pBeautyDlg->UnInitAgora(); + m_pBeautyDlg->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedAudioProfile) == 0) { + m_pAudioProfileDlg->UnInitAgora(); + m_pAudioProfileDlg->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedBeautyAudio) == 0) { + m_pBeautyAudio->UnInitAgora(); + m_pBeautyAudio->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedAudioMixing) == 0) { + m_pAudioMixingDlg->UnInitAgora(); + m_pAudioMixingDlg->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedOriginalVideo) == 0) { + m_pOriginalVideoDlg->UnInitAgora(); + m_pOriginalVideoDlg->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedOriginalAudio) == 0) { + m_pOriginalAudioDlg->UnInitAgora(); + m_pOriginalAudioDlg->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedCustomEncrypt) == 0) { + m_pCustomEncryptDlg->UnInitAgora(); + m_pCustomEncryptDlg->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedMediaPlayer) == 0) { + m_pmediaPlayerDlg->UnInitAgora(); + m_pmediaPlayerDlg->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedVideoProfile) == 0) { + m_pVideoProfileDlg->UnInitAgora(); + m_pVideoProfileDlg->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedMediaEncrypt) == 0) { + m_pMediaEncryptDlg->UnInitAgora(); + m_pMediaEncryptDlg->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedMediaIOCustomVideoCapture) == 0) { + m_pMediaIOVideoDlg->UnInitAgora(); + m_pMediaIOVideoDlg->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedAudioEffect) == 0) { + m_pAudioEffectDlg->UnInitAgora(); + m_pAudioEffectDlg->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedMultiChannel) == 0) { + m_pMultiChannelDlg->UnInitAgora(); + m_pMultiChannelDlg->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedPerCallTest) == 0) { + m_pPerCallTestDlg->UnInitAgora(); + m_pPerCallTestDlg->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedAudioVolume) == 0) { + m_pAudioVolumeDlg->UnInitAgora(); + m_pAudioVolumeDlg->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedReportInCall) == 0) { + m_pReportInCallDlg->UnInitAgora(); + m_pReportInCallDlg->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedRegionConn) == 0) { + m_pRegionConnDlg->UnInitAgora(); + m_pRegionConnDlg->ShowWindow(SW_HIDE); + }else if (str.Compare(advancedCrossChannel) == 0) { + m_pCrossChannelDlg->UnInitAgora(); + m_pCrossChannelDlg->ShowWindow(SW_HIDE); + } +} + +LRESULT CAPIExampleDlg::OnEIDJoinLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + m_bJoinChannel = (BOOL)wParam; + m_lstAdvanced.EnableWindow(!m_bJoinChannel); + m_lstBasicScene.EnableWindow(!m_bJoinChannel); + if (m_bJoinChannel) { + m_stalstInfo.SetWindowText(L"you can leave channel first."); + } + else + m_stalstInfo.SetWindowText(_T("")); + return 0; +} + + +void CAPIExampleDlg::OnBnClickedButtonDocumentWebsite() +{ + ShellExecute(NULL, L"open", L"https://docs.agora.io/cn", NULL, NULL, SW_SHOWNORMAL); +} + + +BOOL CAPIExampleDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} diff --git a/windows/APIExample/APIExample/APIExampleDlg.h b/windows/APIExample/APIExample/APIExampleDlg.h new file mode 100644 index 000000000..45ea4504b --- /dev/null +++ b/windows/APIExample/APIExample/APIExampleDlg.h @@ -0,0 +1,133 @@ + +// APIExampleDlg.h : header file +// + +#pragma once +#include "Basic/LiveBroadcasting/CLiveBroadcastingDlg.h" +#include "Advanced/RTMPinject/AgoraRtmpInjectionDlg.h" +#include "Advanced/RTMPStream/AgoraRtmpStreaming.h" +#include "Advanced/VideoMetadata/CAgoraMetaDataDlg.h" +#include "Advanced/ScreenShare/AgoraScreenCapture.h" +#include "Advanced/CustomAudioCapture/CAgoraCaptureAudioDlg.h" +#include "Advanced/CustomVideoCapture/CAgoraCaptureVideoDlg.h" +#include "Advanced/Beauty/CAgoraBeautyDlg.h" +#include "Advanced/AudioProfile/CAgoraAudioProfile.h" +#include "Advanced/BeautyAudio/CAgoraBeautyAudio.h" +#include "Advanced/AudioMixing/CAgoraAudioMixingDlg.h" +#include "Advanced/OriginalVideo/CAgoraOriginalVideoDlg.h" +#include "Advanced/OriginalAudio/CAgoraOriginalAudioDlg.h" +#include "Advanced/CustomEncrypt/CAgoraCustomEncryptDlg.h" +#include "Advanced/mediaPlayer/CAgoraMediaPlayer.h" +#include "Advanced/VideoProfile/CAgoraVideoProfileDlg.h" +#include "Advanced/MediaEncrypt/CAgoraMediaEncryptDlg.h" +#include "Advanced/MediaIOCustomVideoCaptrue/CAgoraMediaIOVideoCaptureDlg.h" +#include "Advanced/AudioEffect/CAgoraEffectDlg.h" +#include "Advanced/MultiChannel/CAgoraMultiChannelDlg.h" +#include "Advanced/PreCallTest/CAgoraPreCallTestDlg.h" +#include "Advanced/AudioVolume/CAgoraAudioVolumeDlg.h" +#include "Advanced/ReportInCall/CAgoraReportInCallDlg.h" +#include "Advanced/RegionConn/CAgoraRegionConnDlg.h" +#include "Advanced/CrossChannel/CAgoraCrossChannelDlg.h" + + +#include +#include +const int MAIN_AREA_BOTTOM = 15; + +// CAPIExampleDlg dialog +class CAPIExampleDlg : public CDialogEx +{ +// Construction +public: + CAPIExampleDlg(CWnd* pParent = nullptr); // standard constructor + +// Dialog Data +#ifdef AFX_DESIGN_TIME + enum { IDD = IDD_APIEXAMPLE_DIALOG }; +#endif + + protected: + virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support + + +// Implementation +protected: + HICON m_hIcon; + + // Generated message map functions + virtual BOOL OnInitDialog(); + afx_msg void OnSysCommand(UINT nID, LPARAM lParam); + afx_msg void OnPaint(); + afx_msg HCURSOR OnQueryDragIcon(); + DECLARE_MESSAGE_MAP() +public: + afx_msg void OnBnClickedButtonFaq(); + afx_msg void OnBnClickedButtonRegister(); + afx_msg void OnBnClickedButtonDemo(); + afx_msg LRESULT OnEIDJoinLeaveChannel(WPARAM wParam, LPARAM lParam); + +private: + void InitSceneDialog(); + void InitSceneList(); + void InitCtrlText(); + HTREEITEM GetHitItem(NMHDR *pNMHDR); + + void ReleaseScene(CTreeCtrl& treeScene, HTREEITEM& hSelectItem); + void CreateScene(CTreeCtrl& treeScene, CString selectedText); + CLiveBroadcastingDlg *m_pLiveBroadcasting = nullptr; + CAgoraRtmpInjectionDlg *m_pRtmpInjectDlg = nullptr; + CAgoraRtmpStreamingDlg *m_pRtmpStreamingDlg = nullptr; + CAgoraMetaDataDlg *m_pVideoSEIDlg = nullptr; + CAgoraScreenCapture *m_pScreenCap = nullptr; + CAgoraCaptureVideoDlg *m_pCaputreVideoDlg = nullptr; + CAgoraMediaIOVideoCaptureDlg*m_pMediaIOVideoDlg = nullptr; + CAgoraCaptureAduioDlg *m_pCaptureAudioDlg = nullptr; + CAgoraBeautyDlg *m_pBeautyDlg = nullptr; + CAgoraAudioProfile *m_pAudioProfileDlg = nullptr; + CAgoraBeautyAudio *m_pBeautyAudio = nullptr; + CAgoraAudioMixingDlg *m_pAudioMixingDlg = nullptr; + CAgoraOriginalVideoDlg *m_pOriginalVideoDlg = nullptr; + CAgoraOriginalAudioDlg *m_pOriginalAudioDlg = nullptr; + CAgoraCustomEncryptDlg *m_pCustomEncryptDlg = nullptr; + CAgoraMediaPlayer *m_pmediaPlayerDlg = nullptr; + CAgoraVideoProfileDlg *m_pVideoProfileDlg = nullptr; + CAgoraMediaEncryptDlg *m_pMediaEncryptDlg = nullptr; + CAgoraEffectDlg *m_pAudioEffectDlg = nullptr; + CAgoraMultiChannelDlg *m_pMultiChannelDlg = nullptr; + CAgoraPreCallTestDlg *m_pPerCallTestDlg = nullptr; + CAgoraAudioVolumeDlg *m_pAudioVolumeDlg = nullptr; + CAgoraReportInCallDlg *m_pReportInCallDlg = nullptr; + CAgoraRegionConnDlg *m_pRegionConnDlg = nullptr; + CAgoraCrossChannelDlg *m_pCrossChannelDlg = nullptr; + + CString m_preSelectedItemText = _T(""); + std::vector m_vecBasic, m_vecAdvanced; + + + + BOOL m_bJoinChannel = FALSE; +public: + CStatic m_staMainArea; + CTreeCtrl m_lstBasicScene; + CTreeCtrl m_lstAdvanced; + + afx_msg void OnClickListBasic(NMHDR *pNMHDR, LRESULT *pResult); + afx_msg void OnSelchangedListAdvanced(NMHDR *pNMHDR, LRESULT *pResult); + afx_msg void OnClickListAdvanced(NMHDR *pNMHDR, LRESULT *pResult); + + afx_msg void OnSelchangedListBasic(NMHDR *pNMHDR, LRESULT *pResult); + afx_msg void OnSelchangingListBasic(NMHDR *pNMHDR, LRESULT *pResult); + + afx_msg void OnSelchangingListAdvanced(NMHDR *pNMHDR, LRESULT *pResult); + CStatic m_stalstInfo; + afx_msg void OnBnClickedButtonDocumentWebsite(); + CStatic m_staAdvancedScene; + CStatic m_grpBasicScene; + CButton m_btnDemoWebSite; + CButton m_btnRegister; + CButton m_btnFAQ; + CButton m_btnDocWebsite; + CStatic m_grpDoc; + virtual BOOL PreTranslateMessage(MSG* pMsg); +}; + diff --git a/windows/APIExample/APIExample/Advanced/AudioEffect/CAgoraEffectDlg.cpp b/windows/APIExample/APIExample/Advanced/AudioEffect/CAgoraEffectDlg.cpp new file mode 100644 index 000000000..06312f845 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/AudioEffect/CAgoraEffectDlg.cpp @@ -0,0 +1,709 @@ +#include "stdafx.h" +#include "APIExample.h" +#include "CAgoraEffectDlg.h" + + +IMPLEMENT_DYNAMIC(CAgoraEffectDlg, CDialogEx) + +CAgoraEffectDlg::CAgoraEffectDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_AUDIO_EFFECT, pParent) +{ + +} + +CAgoraEffectDlg::~CAgoraEffectDlg() +{ +} + +void CAgoraEffectDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannel); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannel); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_STATIC_AUDIO_EFFECT_PATH, m_staEffectPath); + DDX_Control(pDX, IDC_EDIT_AUDIO_EFFECT_PATH, m_edtEffectPath); + DDX_Control(pDX, IDC_BUTTON_ADD_EFFECT, m_btnAddEffect); + DDX_Control(pDX, IDC_BUTTON_PRELOAD, m_btnPreLoad); + DDX_Control(pDX, IDC_BUTTON_UNLOAD_EFFECT, m_btnUnload); + DDX_Control(pDX, IDC_BUTTON_REMOVE, m_btnRemove); + DDX_Control(pDX, IDC_BUTTON_PAUSE_EFFECT, m_btnPause); + DDX_Control(pDX, IDC_BUTTON_RESUME_EFFECT, m_btnResume); + DDX_Control(pDX, IDC_STATIC_DETAIL, m_staDetails); + DDX_Control(pDX, IDC_STATIC_AUDIO_REPEAT, m_staLoops); + DDX_Control(pDX, IDC_EDIT_AUDIO_REPEAT_TIMES, m_edtLoops); + DDX_Control(pDX, IDC_STATIC_AUDIO_AGIN, m_staGain); + DDX_Control(pDX, IDC_EDIT_AUDIO_AGIN, m_edtGain); + DDX_Control(pDX, IDC_SPIN_AGIN, m_spinGain); + DDX_Control(pDX, IDC_STATIC_AUDIO_PITCH, m_staPitch); + DDX_Control(pDX, IDC_EDIT_AUDIO_PITCH, m_edtPitch); + DDX_Control(pDX, IDC_SPIN_PITCH, m_spinPitch); + DDX_Control(pDX, IDC_STATIC_AUDIO_PAN, m_staPan); + DDX_Control(pDX, IDC_COMBO_PAN, m_cmbPan); + DDX_Control(pDX, IDC_CHK_PUBLISH, m_chkPublish); + DDX_Control(pDX, IDC_BUTTON_PLAY_EFFECT, m_btnPlay); + DDX_Control(pDX, IDC_BUTTON_PAUSE_ALL_EFFECT, m_btnPauseAll); + DDX_Control(pDX, IDC_BUTTON_STOP_ALL_EFFECT2, m_btnStopAll); + DDX_Control(pDX, IDC_BUTTON_STOP_EFFECT, m_btnStopEffect); + DDX_Control(pDX, IDC_STATIC_AUDIO_EFFECT, m_staEffect); + DDX_Control(pDX, IDC_COMBO2, m_cmbEffect); + DDX_Control(pDX, IDC_STATIC_AUDIO_VLOUME, m_staVolume); + DDX_Control(pDX, IDC_SLIDER_VLOUME, m_sldVolume); +} + + +BEGIN_MESSAGE_MAP(CAgoraEffectDlg, CDialogEx) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraEffectDlg::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraEffectDlg::OnEIDJoinChannelSuccess) + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraEffectDlg::OnBnClickedButtonJoinchannel) + ON_BN_CLICKED(IDC_BUTTON_ADD_EFFECT, &CAgoraEffectDlg::OnBnClickedButtonAddEffect) + ON_BN_CLICKED(IDC_BUTTON_PRELOAD, &CAgoraEffectDlg::OnBnClickedButtonPreload) + ON_BN_CLICKED(IDC_BUTTON_UNLOAD_EFFECT, &CAgoraEffectDlg::OnBnClickedButtonUnloadEffect) + ON_BN_CLICKED(IDC_BUTTON_REMOVE, &CAgoraEffectDlg::OnBnClickedButtonRemove) + ON_BN_CLICKED(IDC_BUTTON_PAUSE_EFFECT, &CAgoraEffectDlg::OnBnClickedButtonPauseEffect) + ON_BN_CLICKED(IDC_BUTTON_RESUME_EFFECT, &CAgoraEffectDlg::OnBnClickedButtonResumeEffect) + ON_BN_CLICKED(IDC_BUTTON_PLAY_EFFECT, &CAgoraEffectDlg::OnBnClickedButtonPlayEffect) + ON_BN_CLICKED(IDC_BUTTON_PAUSE_ALL_EFFECT, &CAgoraEffectDlg::OnBnClickedButtonPauseAllEffect) + ON_BN_CLICKED(IDC_BUTTON_STOP_ALL_EFFECT2, &CAgoraEffectDlg::OnBnClickedButtonStopAllEffect2) + ON_NOTIFY(UDN_DELTAPOS, IDC_SPIN_AGIN, &CAgoraEffectDlg::OnDeltaposSpinGain) + ON_NOTIFY(UDN_DELTAPOS, IDC_SPIN_PITCH, &CAgoraEffectDlg::OnDeltaposSpinPitch) + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraEffectDlg::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraEffectDlg::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraEffectDlg::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraEffectDlg::OnEIDUserOffline) + ON_MESSAGE(WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), &CAgoraEffectDlg::OnEIDRemoteVideoStateChanged) + + ON_LBN_SELCHANGE(IDC_LIST_INFO_BROADCASTING, &CAgoraEffectDlg::OnSelchangeListInfoBroadcasting) + ON_WM_SHOWWINDOW() + ON_BN_CLICKED(IDC_BUTTON_STOP_EFFECT, &CAgoraEffectDlg::OnBnClickedButtonStopEffect) + ON_NOTIFY(NM_RELEASEDCAPTURE, IDC_SLIDER_VLOUME, &CAgoraEffectDlg::OnReleasedcaptureSliderVolume) +END_MESSAGE_MAP() + + +//Initialize the Ctrl Text. +void CAgoraEffectDlg::InitCtrlText() +{ + m_staChannel.SetWindowText(commonCtrlChannel); + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + m_staEffectPath.SetWindowText(AudioEffectCtrlEffectPath); + m_staEffect.SetWindowText(AudioEffectCtrlEffect); + m_staGain.SetWindowText(AudioEffectCtrlGain); + m_staPan.SetWindowText(AudioEffectCtrlPan); + m_staPitch.SetWindowText(AudioEffectCtrlPitch); + m_staLoops.SetWindowText(AudioEffectCtrlLoops); + m_chkPublish.SetWindowText(AudioEffectCtrlPublish); + m_btnAddEffect.SetWindowText(AudioEffectCtrlAddEffect); + m_btnPause.SetWindowText(AudioEffectCtrlPauseEffect); + m_btnRemove.SetWindowText(AudioEffectCtrlRemoveEffect); + m_btnPlay.SetWindowText(AudioEffectCtrlPlayEffect); + m_btnPauseAll.SetWindowText(AudioEffectCtrlPauseAllEffect); + m_btnPreLoad.SetWindowText(AudioEffectCtrlPreLoad); + m_btnUnload.SetWindowText(AudioEffectCtrlUnPreload); + m_btnResume.SetWindowText(AudioEffectCtrlResumeEffect); + m_btnStopAll.SetWindowText(AudioEffectCtrlStopAllEffect); + m_btnStopEffect.SetWindowText(AudioEffectCtrlStopEffect); + m_staVolume.SetWindowText(AudioEffectCtrlVolume); +} + + + +//Initialize the Agora SDK +bool CAgoraEffectDlg::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //enable video in the engine. + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + return true; +} + + +//UnInitialize the Agora SDK +void CAgoraEffectDlg::UnInitAgora() +{ + if (m_rtcEngine) { + if (m_joinChannel) + //leave channel + m_joinChannel = !m_rtcEngine->leaveChannel(); + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} + +//render local video from SDK local capture. +void CAgoraEffectDlg::RenderLocalVideo() +{ + if (m_rtcEngine) { + //start preview in the engine. + m_rtcEngine->startPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("startPreview")); + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_localVideoWnd.GetSafeHwnd(); + //setup local video in the engine to canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setupLocalVideo")); + } +} + + +//resume window status +void CAgoraEffectDlg::ResumeStatus() +{ + InitCtrlText(); + m_lstInfo.ResetContent(); + m_edtChannel.SetWindowText(_T("")); + m_edtEffectPath.SetWindowText(_T("")); + m_edtGain.SetWindowText(_T("100.0")); + m_edtLoops.SetWindowText(_T("0")); + m_edtPitch.SetWindowText(_T("1.0")); + m_cmbPan.SetCurSel(0); + m_cmbEffect.ResetContent(); + m_chkPublish.SetCheck(TRUE); + m_btnPauseAll.SetWindowText(AudioEffectCtrlPauseAllEffect); + m_pauseAll = false; + m_joinChannel = false; + m_initialize = false; + m_audioMixing = false; +} + +void CAgoraEffectDlg::OnBnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + CString strInfo; + if (!m_joinChannel) { + CString strChannelName; + m_edtChannel.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + std::string szChannelId = cs2utf8(strChannelName); + //join channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + strInfo.Format(_T("join channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + } + } + else { + //leave channel in the engine. + if (0 == m_rtcEngine->leaveChannel()) { + strInfo.Format(_T("leave channel %s"), getCurrentTime()); + } + } + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + +//add effect button click handler +void CAgoraEffectDlg::OnBnClickedButtonAddEffect() +{ + CString strPath; + m_edtEffectPath.GetWindowText(strPath); + //judge file is exists. + if (!strPath.IsEmpty()) + { + m_cmbEffect.InsertString(m_cmbEffect.GetCount(), strPath); + m_mapEffect.insert(std::make_pair(strPath, m_soundId++)); + } + else { + MessageBox(_T("url can not empty.")); + } + m_cmbEffect.SetCurSel(0); +} + + +//pre load button click handler +void CAgoraEffectDlg::OnBnClickedButtonPreload() +{ + if (m_cmbEffect.GetCurSel() < 0) + { + return; + } + CString strEffect; + m_cmbEffect.GetWindowText(strEffect); + std::string strPath = cs2utf8(strEffect); + //pre load effect + int nRet = m_rtcEngine->preloadEffect(m_mapEffect[strEffect], strPath.c_str()); + CString strInfo; + strInfo.Format(_T("preload effect :path:%s"), strEffect); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + +//un load button click handler +void CAgoraEffectDlg::OnBnClickedButtonUnloadEffect() +{ + if (m_cmbEffect.GetCurSel() < 0) + { + return; + } + CString strEffect; + m_cmbEffect.GetWindowText(strEffect); + // un load effect + m_rtcEngine->unloadEffect(m_mapEffect[strEffect]); + CString strInfo; + strInfo.Format(_T("unload effect :path:%s"), strEffect); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + + +//remove effect button click handler. +void CAgoraEffectDlg::OnBnClickedButtonRemove() +{ + if (m_cmbEffect.GetCurSel() < 0) + { + return; + } + CString strEffect; + m_cmbEffect.GetWindowText(strEffect); + m_cmbEffect.DeleteString(m_cmbEffect.GetCurSel()); + CString strInfo; + strInfo.Format(_T("remove effect :path:%s"), strEffect); + m_mapEffect.erase(m_mapEffect.find(strEffect)); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_cmbEffect.SetCurSel(0); +} + +//pause effect button click handler. +void CAgoraEffectDlg::OnBnClickedButtonPauseEffect() +{ + if (m_cmbEffect.GetCurSel() < 0) + { + return; + } + CString strEffect; + m_cmbEffect.GetWindowText(strEffect); + //pause effect by sound id + m_rtcEngine->pauseEffect(m_mapEffect[strEffect]); + + CString strInfo; + strInfo.Format(_T("pause effect :path:%s"), strEffect); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + + +//resume effect button click handler. +void CAgoraEffectDlg::OnBnClickedButtonResumeEffect() +{ + if (m_cmbEffect.GetCurSel() < 0) + { + return; + } + CString strEffect; + m_cmbEffect.GetWindowText(strEffect); + // resume effect by sound id. + m_rtcEngine->resumeEffect(m_mapEffect[strEffect]); + + CString strInfo; + strInfo.Format(_T("resume effect :path:%s"), strEffect); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + +//play effect button click handler. +void CAgoraEffectDlg::OnBnClickedButtonPlayEffect() +{ + if (m_cmbEffect.GetCurSel() < 0) + { + return; + } + CString strEffect; + m_cmbEffect.GetWindowText(strEffect); + std::string strFile; + strFile = cs2utf8(strEffect).c_str(); + CString strLoops; + m_edtLoops.GetWindowText(strLoops); + int loops = _ttol(strLoops); + + CString strPitch; + m_edtPitch.GetWindowText(strPitch); + double pitch = _ttof(strPitch); + + CString strGain; + m_edtGain.GetWindowText(strGain); + int gain = _ttol(strGain); + + CString strPan; + m_cmbPan.GetWindowText(strPan); + double pan = _ttof(strPan); + + BOOL publish = m_chkPublish.GetCheck(); + //play effect by effect path. + int nRet = m_rtcEngine->playEffect(m_mapEffect[strEffect], strFile.c_str(), + loops, pitch, pan, gain, publish); + CString strInfo; + strInfo.Format(_T("play effect :path:%s,loops:%d,pitch:%.1f,pan:%.0f,gain:%d,publish:%d"), + strEffect, loops, pitch, pan, gain, publish); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + +//stop effect button click handler. +void CAgoraEffectDlg::OnBnClickedButtonStopEffect() +{ + if (m_cmbEffect.GetCurSel() < 0) + { + return; + } + CString strEffect; + m_cmbEffect.GetWindowText(strEffect); + //stop effect by sound id. + m_rtcEngine->stopEffect(m_mapEffect[strEffect]); + + CString strInfo; + strInfo.Format(_T("stop effect :path:%s"), strEffect); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + +//pause all effect button click handler. +void CAgoraEffectDlg::OnBnClickedButtonPauseAllEffect() +{ + if (!m_pauseAll) + { + //pause all effect + m_rtcEngine->pauseAllEffects(); + CString strInfo; + strInfo.Format(_T("pause All Effects")); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_btnPauseAll.SetWindowText(AudioEffectCtrlResumeEffect); + } + else { + //resume all effect + m_rtcEngine->resumeAllEffects(); + CString strInfo; + strInfo.Format(_T("resume All Effects")); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_btnPauseAll.SetWindowText(AudioEffectCtrlPauseAllEffect); + } + m_pauseAll = !m_pauseAll; +} + +//stop all effect button click handler. +void CAgoraEffectDlg::OnBnClickedButtonStopAllEffect2() +{ + //stop all effect + m_rtcEngine->stopAllEffects(); + CString strInfo; + strInfo.Format(_T("stop All Effects")); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + + +void CAgoraEffectDlg::OnDeltaposSpinGain(NMHDR *pNMHDR, LRESULT *pResult) +{ + LPNMUPDOWN pNMUpDown = reinterpret_cast(pNMHDR); + CString strGain; + m_edtGain.GetWindowText(strGain); + double gain = _ttof(strGain); + if ((pNMUpDown->iDelta < 0)) + gain = (gain + 0.1 <= 100 ? gain + 0.1 : gain); + if ((pNMUpDown->iDelta > 0)) + gain = (gain - 0.1 >= 0.0 ? gain - 0.1 : gain); + strGain.Format(_T("%.1f"), gain); + m_edtGain.SetWindowText(strGain); + *pResult = 0; +} + + +void CAgoraEffectDlg::OnDeltaposSpinPitch(NMHDR *pNMHDR, LRESULT *pResult) +{ + LPNMUPDOWN pNMUpDown = reinterpret_cast(pNMHDR); + CString strPitch; + m_edtPitch.GetWindowText(strPitch); + double pitch = _ttof(strPitch); + if ((pNMUpDown->iDelta < 0)) + pitch = (pitch + 1 <= 100 ? pitch + 1 : pitch); + if ((pNMUpDown->iDelta > 0)) + pitch = (pitch - 1 >= 0 ? pitch - 1 : pitch); + strPitch.Format(_T("%.1f"), pitch); + m_edtPitch.SetWindowText(strPitch); + *pResult = 0; +} + + +void CAgoraEffectDlg::OnSelchangeListInfoBroadcasting() +{ + int sel = m_lstInfo.GetCurSel(); + if (sel < 0)return; + CString strDetail; + m_lstInfo.GetText(sel, strDetail); + m_staDetails.SetWindowText(strDetail); +} + + +void CAgoraEffectDlg::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow)//bShwo is true ,show window + { + InitCtrlText(); + RenderLocalVideo(); + } + else { + ResumeStatus(); + } + +} + + +BOOL CAgoraEffectDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + m_localVideoWnd.MoveWindow(&rcArea); + m_localVideoWnd.ShowWindow(SW_SHOW); + int nIndex = 0; + + m_cmbPan.InsertString(nIndex++, _T("0")); + m_cmbPan.InsertString(nIndex++, _T("-1")); + m_cmbPan.InsertString(nIndex++, _T("1")); + ResumeStatus(); + m_sldVolume.SetRange(0, 100); + return TRUE; +} + + +BOOL CAgoraEffectDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} + +//EID_JOINCHANNEL_SUCCESS message window handler +LRESULT CAgoraEffectDlg::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_btnJoinChannel.EnableWindow(TRUE); + m_joinChannel = true; + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + m_btnJoinChannel.EnableWindow(TRUE); + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_localVideoWnd.SetUID(wParam); + //notify parent window + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); + return 0; +} + +//EID_LEAVEHANNEL_SUCCESS message window handler +LRESULT CAgoraEffectDlg::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + m_btnJoinChannel.EnableWindow(TRUE); + m_joinChannel = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + CString strInfo; + strInfo.Format(_T("leave channel success %s"), getCurrentTime()); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return 0; +} + +//EID_USER_JOINED message window handler +LRESULT CAgoraEffectDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + CString strInfo; + strInfo.Format(_T("%u joined"), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + +//EID_USER_OFFLINE message handler. +LRESULT CAgoraEffectDlg::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + m_rtcEngine->setupRemoteVideo(canvas); + CString strInfo; + strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + +//EID_REMOTE_VIDEO_STATE_CHANED message window handler. +LRESULT CAgoraEffectDlg::OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam) +{ + PVideoStateStateChanged stateChanged = (PVideoStateStateChanged)wParam; + if (stateChanged) { + //onRemoteVideoStateChanged + CString strSateInfo; + switch (stateChanged->state) { + case REMOTE_VIDEO_STATE_STARTING: + strSateInfo = _T("REMOTE_VIDEO_STATE_STARTING"); + break; + case REMOTE_VIDEO_STATE_STOPPED: + strSateInfo = _T("strSateInfo"); + break; + case REMOTE_VIDEO_STATE_DECODING: + strSateInfo = _T("REMOTE_VIDEO_STATE_DECODING"); + break; + case REMOTE_VIDEO_STATE_FAILED: + strSateInfo = _T("REMOTE_VIDEO_STATE_FAILED "); + break; + case REMOTE_VIDEO_STATE_FROZEN: + strSateInfo = _T("REMOTE_VIDEO_STATE_FROZEN "); + break; + } + CString strInfo; + strInfo.Format(_T("onRemoteVideoStateChanged: uid=%u, %s"), stateChanged->uid, strSateInfo); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + return 0; +} + + + +/* +note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one +parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). +*/ +void CAudioEffectEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } +} +/* +note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. +parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). +*/ +void CAudioEffectEventHandler::onUserJoined(uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } +} + +/* +note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. +parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. +*/ +void CAudioEffectEventHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } +} +/* +note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. +parameters: + stats: Call statistics. +*/ + +void CAudioEffectEventHandler::onLeaveChannel(const RtcStats& stats) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } +} +/** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. +*/ +void CAudioEffectEventHandler::onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) +{ + if (m_hMsgHanlder) { + PVideoStateStateChanged stateChanged = new VideoStateStateChanged; + stateChanged->uid = uid; + stateChanged->reason = reason; + stateChanged->state = state; + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), (WPARAM)stateChanged, 0); + } +} + + + +void CAgoraEffectDlg::OnReleasedcaptureSliderVolume(NMHDR *pNMHDR, LRESULT *pResult) +{ + LPNMCUSTOMDRAW pNMCD = reinterpret_cast(pNMHDR); + int pos = m_sldVolume.GetPos(); + m_rtcEngine->setEffectsVolume(pos); + //m_mediaPlayer->seek(pos); + *pResult = 0; +} diff --git a/windows/APIExample/APIExample/Advanced/AudioEffect/CAgoraEffectDlg.h b/windows/APIExample/APIExample/Advanced/AudioEffect/CAgoraEffectDlg.h new file mode 100644 index 000000000..55ee99750 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/AudioEffect/CAgoraEffectDlg.h @@ -0,0 +1,176 @@ +#pragma once +#include "AGVideoWnd.h" +#include + +class CAudioEffectEventHandler : public IRtcEngineEventHandler +{ +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override; + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messgaing SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override; + /** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. + */ + virtual void onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) override; +private: + HWND m_hMsgHanlder; +}; + + +class CAgoraEffectDlg : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraEffectDlg) + +public: + CAgoraEffectDlg(CWnd* pParent = nullptr); + virtual ~CAgoraEffectDlg(); + + enum { IDD = IDD_DIALOG_AUDIO_EFFECT }; +public: + //Initialize the Ctrl Text. + void InitCtrlText(); + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //render local video from SDK local capture. + void RenderLocalVideo(); + //resume window status + void ResumeStatus(); + +private: + bool m_joinChannel = false; + bool m_initialize = false; + bool m_audioMixing = false; + bool m_pauseAll = false; + IRtcEngine* m_rtcEngine = nullptr; + CAGVideoWnd m_localVideoWnd; + CAudioEffectEventHandler m_eventHandler; + int m_soundId = 0; + std::map m_mapEffect; + +protected: + virtual void DoDataExchange(CDataExchange* pDX); + DECLARE_MESSAGE_MAP() + LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam); +public: + CStatic m_staVideoArea; + CListBox m_lstInfo; + CStatic m_staChannel; + CEdit m_edtChannel; + CButton m_btnJoinChannel; + CStatic m_staEffectPath; + CEdit m_edtEffectPath; + CButton m_btnAddEffect; + CButton m_btnPreLoad; + CButton m_btnUnload; + CButton m_btnRemove; + CButton m_btnPause; + CButton m_btnResume; + CStatic m_staDetails; + CStatic m_staLoops; + CEdit m_edtLoops; + CStatic m_staGain; + CEdit m_edtGain; + CSpinButtonCtrl m_spinGain; + CStatic m_staPitch; + CEdit m_edtPitch; + CSpinButtonCtrl m_spinPitch; + CStatic m_staPan; + CComboBox m_cmbPan; + CButton m_chkPublish; + CButton m_btnPlay; + CButton m_btnPauseAll; + CButton m_btnStopAll; + CButton m_btnStopEffect; + afx_msg void OnBnClickedButtonJoinchannel(); + afx_msg void OnBnClickedButtonAddEffect(); + afx_msg void OnBnClickedButtonPreload(); + afx_msg void OnBnClickedButtonUnloadEffect(); + afx_msg void OnBnClickedButtonRemove(); + afx_msg void OnBnClickedButtonPauseEffect(); + afx_msg void OnBnClickedButtonResumeEffect(); + afx_msg void OnBnClickedButtonPlayEffect(); + afx_msg void OnBnClickedButtonStopEffect(); + afx_msg void OnBnClickedButtonPauseAllEffect(); + afx_msg void OnBnClickedButtonStopAllEffect2(); + afx_msg void OnDeltaposSpinGain(NMHDR *pNMHDR, LRESULT *pResult); + afx_msg void OnDeltaposSpinPitch(NMHDR *pNMHDR, LRESULT *pResult); + afx_msg void OnSelchangeListInfoBroadcasting(); + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + virtual BOOL OnInitDialog(); + virtual BOOL PreTranslateMessage(MSG* pMsg); + CStatic m_staEffect; + CComboBox m_cmbEffect; + CStatic m_staVolume; + CSliderCtrl m_sldVolume; + afx_msg void OnReleasedcaptureSliderVolume(NMHDR *pNMHDR, LRESULT *pResult); +}; diff --git a/windows/APIExample/APIExample/Advanced/AudioMixing/CAgoraAudioMixingDlg.cpp b/windows/APIExample/APIExample/Advanced/AudioMixing/CAgoraAudioMixingDlg.cpp new file mode 100644 index 000000000..c0afa9b00 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/AudioMixing/CAgoraAudioMixingDlg.cpp @@ -0,0 +1,483 @@ +#include "stdafx.h" +#include "APIExample.h" +#include "CAgoraAudioMixingDlg.h" + + + +IMPLEMENT_DYNAMIC(CAgoraAudioMixingDlg, CDialogEx) + +CAgoraAudioMixingDlg::CAgoraAudioMixingDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_AUDIO_MIX, pParent) +{ + +} + +CAgoraAudioMixingDlg::~CAgoraAudioMixingDlg() +{ +} + + +//Initialize the Ctrl Text. +void CAgoraAudioMixingDlg::InitCtrlText() +{ + m_staChannel.SetWindowText(commonCtrlChannel); + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + m_btnSetAudioMix.SetWindowText(audioMixingCtrlSetAudioMixing); + m_staAudioMix.SetWindowText(audioMixingCtrlMixingPath); + m_staAudioRepeat.SetWindowText(audioMixingCtrlRepeatTimes); + m_chkOnlyLocal.SetWindowText(audioMixingCtrlOnlyLocal); + m_chkMicroPhone.SetWindowText(audioMixingCtrlReplaceMicroPhone); + m_staVolume.SetWindowTextW(AudioEffectCtrlVolume); +} + + + +//Initialize the Agora SDK +bool CAgoraAudioMixingDlg::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //enable video in the engine. + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + return true; +} + + +//UnInitialize the Agora SDK +void CAgoraAudioMixingDlg::UnInitAgora() +{ + if (m_rtcEngine) { + if (m_joinChannel) + //leave channel + m_joinChannel = !m_rtcEngine->leaveChannel(); + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} + +//render local video from SDK local capture. +void CAgoraAudioMixingDlg::RenderLocalVideo() +{ + if (m_rtcEngine) { + //start preview in the engine. + m_rtcEngine->startPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("startPreview")); + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_localVideoWnd.GetSafeHwnd(); + //setup local video in the engine to canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setupLocalVideo")); + } +} + + +//resume window status +void CAgoraAudioMixingDlg::ResumeStatus() +{ + InitCtrlText(); + m_lstInfo.ResetContent(); + m_staDetail.SetWindowText(_T("")); + m_edtChannel.SetWindowText(_T("")); + m_edtRepatTimes.SetWindowText(_T("")); + m_chkOnlyLocal.SetCheck(BST_UNCHECKED); + m_chkOnlyLocal.EnableWindow(TRUE); + m_chkMicroPhone.SetCheck(BST_UNCHECKED); + m_chkMicroPhone.EnableWindow(TRUE); + m_joinChannel = false; + m_initialize = false; + m_audioMixing = false; +} + + +void CAgoraAudioMixingDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); + DDX_Control(pDX, IDC_STATIC_DETAIL, m_staDetail); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannel); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannel); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_STATIC_AUDIO_MIX, m_staAudioMix); + DDX_Control(pDX, IDC_STATIC_AUDIO_REPEAT, m_staAudioRepeat); + DDX_Control(pDX, IDC_EDIT_AUDIO_MIX_PATH, m_edtAudioMix); + DDX_Control(pDX, IDC_BUTTON_SET_AUDIO_MIX, m_btnSetAudioMix); + DDX_Control(pDX, IDC_EDIT_AUDIO_REPEAT_TIMES, m_edtRepatTimes); + DDX_Control(pDX, IDC_CHK_ONLY_LOCAL, m_chkOnlyLocal); + DDX_Control(pDX, IDC_CHK_REPLACE_MICROPHONE, m_chkMicroPhone); + DDX_Control(pDX, IDC_STATIC_AUDIO_VOLUME, m_staVolume); + DDX_Control(pDX, IDC_SLIDER_VOLUME, m_sldVolume); +} + + +BEGIN_MESSAGE_MAP(CAgoraAudioMixingDlg, CDialogEx) + ON_LBN_SELCHANGE(IDC_LIST_INFO_BROADCASTING, &CAgoraAudioMixingDlg::OnSelchangeListInfoBroadcasting) + ON_WM_SHOWWINDOW() + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraAudioMixingDlg::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraAudioMixingDlg::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraAudioMixingDlg::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraAudioMixingDlg::OnEIDUserOffline) + ON_MESSAGE(WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), &CAgoraAudioMixingDlg::OnEIDRemoteVideoStateChanged) + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraAudioMixingDlg::OnBnClickedButtonJoinchannel) + ON_BN_CLICKED(IDC_BUTTON_SET_AUDIO_MIX, &CAgoraAudioMixingDlg::OnBnClickedButtonSetAudioMix) + ON_NOTIFY(NM_RELEASEDCAPTURE, IDC_SLIDER_VOLUME, &CAgoraAudioMixingDlg::OnReleasedcaptureSliderVolume) +END_MESSAGE_MAP() + + +void CAgoraAudioMixingDlg::OnSelchangeListInfoBroadcasting() +{ + int sel = m_lstInfo.GetCurSel(); + if (sel < 0)return; + CString strDetail; + m_lstInfo.GetText(sel, strDetail); + m_staDetail.SetWindowText(strDetail); +} + + +void CAgoraAudioMixingDlg::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow)//bShwo is true ,show window + { + InitCtrlText(); + RenderLocalVideo(); + } + else { + ResumeStatus(); + } +} + + +BOOL CAgoraAudioMixingDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + m_localVideoWnd.MoveWindow(&rcArea); + m_localVideoWnd.ShowWindow(SW_SHOW); + m_sldVolume.SetRange(0, 100); + ResumeStatus(); + return TRUE; +} + + +BOOL CAgoraAudioMixingDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} + + +void CAgoraAudioMixingDlg::OnBnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + CString strInfo; + if (!m_joinChannel) { + CString strChannelName; + m_edtChannel.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + std::string szChannelId = cs2utf8(strChannelName); + //join channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + strInfo.Format(_T("join channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + } + } + else { + //leave channel in the engine. + if (0 == m_rtcEngine->leaveChannel()) { + strInfo.Format(_T("leave channel %s"), getCurrentTime()); + } + } + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + + +void CAgoraAudioMixingDlg::OnBnClickedButtonSetAudioMix() +{ + CString strPath; + m_edtAudioMix.GetWindowText(strPath); + std::string strAudioPath = cs2utf8(strPath); + BOOL bOnlyLocal = FALSE; + BOOL bReplaceMicroPhone = TRUE; + int iRepeatTimes = 1; + if (!m_audioMixing) + { + if (strAudioPath.empty()) + { + AfxMessageBox(_T("audio path can not empty.")); + return; + } + bOnlyLocal = m_chkOnlyLocal.GetCheck() ? TRUE : FALSE; + bReplaceMicroPhone = m_chkMicroPhone.GetCheck() ? TRUE : FALSE; + CString strTimes; + CString strInfo; + m_edtRepatTimes.GetWindowText(strTimes); + iRepeatTimes = _ttoi(strTimes); + //start audio mixing in the engine. + int nRet = m_rtcEngine->startAudioMixing(strAudioPath.c_str(), + bOnlyLocal, + bReplaceMicroPhone, + iRepeatTimes + ); + strInfo.Format(_T("path:%s,\nonlyLocal:%s,\nReplaceMicroPhone:%s,\nRepeatTimes:%d"), strPath, + bOnlyLocal?_T("TRUE"):_T("FALSE"), bReplaceMicroPhone?_T("TRUE"):_T("FALSE"), + iRepeatTimes); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_btnSetAudioMix.SetWindowText(audioMixingCtrlUnSetAudioMixing); + m_chkMicroPhone.EnableWindow(FALSE); + m_chkOnlyLocal.EnableWindow(FALSE); + } + else { + //stop audio mixing in the engine. + m_rtcEngine->stopAudioMixing(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("cancel audio mixing")); + m_btnSetAudioMix.SetWindowText(audioMixingCtrlSetAudioMixing); + m_chkOnlyLocal.EnableWindow(TRUE); + m_chkMicroPhone.EnableWindow(TRUE); + + } + m_audioMixing = !m_audioMixing; +} + + + +//EID_JOINCHANNEL_SUCCESS message window handler +LRESULT CAgoraAudioMixingDlg::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = true; + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + m_btnJoinChannel.EnableWindow(TRUE); + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_localVideoWnd.SetUID(wParam); + //notify parent window + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); + return 0; +} + +//EID_LEAVEHANNEL_SUCCESS message window handler +LRESULT CAgoraAudioMixingDlg::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + CString strInfo; + strInfo.Format(_T("leave channel success %s"), getCurrentTime()); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return 0; +} + +//EID_USER_JOINED message window handler +LRESULT CAgoraAudioMixingDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + CString strInfo; + strInfo.Format(_T("%u joined"), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + +//EID_USER_OFFLINE message handler. +LRESULT CAgoraAudioMixingDlg::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + m_rtcEngine->setupRemoteVideo(canvas); + CString strInfo; + strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + +//EID_REMOTE_VIDEO_STATE_CHANED message window handler. +LRESULT CAgoraAudioMixingDlg::OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam) +{ + PVideoStateStateChanged stateChanged = (PVideoStateStateChanged)wParam; + if (stateChanged) { + //onRemoteVideoStateChanged + CString strSateInfo; + switch (stateChanged->state) { + case REMOTE_VIDEO_STATE_STARTING: + strSateInfo = _T("REMOTE_VIDEO_STATE_STARTING"); + break; + case REMOTE_VIDEO_STATE_STOPPED: + strSateInfo = _T("strSateInfo"); + break; + case REMOTE_VIDEO_STATE_DECODING: + strSateInfo = _T("REMOTE_VIDEO_STATE_DECODING"); + break; + case REMOTE_VIDEO_STATE_FAILED: + strSateInfo = _T("REMOTE_VIDEO_STATE_FAILED "); + break; + case REMOTE_VIDEO_STATE_FROZEN: + strSateInfo = _T("REMOTE_VIDEO_STATE_FROZEN "); + break; + } + CString strInfo; + strInfo.Format(_T("onRemoteVideoStateChanged: uid=%u, %s"), stateChanged->uid, strSateInfo); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + return 0; +} + + + +/* +note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one +parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). +*/ +void CAudioMixingEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } +} +/* +note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. +parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). +*/ +void CAudioMixingEventHandler::onUserJoined(uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } +} + +/* +note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. +parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. +*/ +void CAudioMixingEventHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } +} +/* +note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. +parameters: + stats: Call statistics. +*/ + +void CAudioMixingEventHandler::onLeaveChannel(const RtcStats& stats) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } +} +/** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. +*/ +void CAudioMixingEventHandler::onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) +{ + if (m_hMsgHanlder) { + PVideoStateStateChanged stateChanged = new VideoStateStateChanged; + stateChanged->uid = uid; + stateChanged->reason = reason; + stateChanged->state = state; + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), (WPARAM)stateChanged, 0); + } +} + + +void CAgoraAudioMixingDlg::OnReleasedcaptureSliderVolume(NMHDR *pNMHDR, LRESULT *pResult) +{ + LPNMCUSTOMDRAW pNMCD = reinterpret_cast(pNMHDR); + int pos = m_sldVolume.GetPos(); + m_rtcEngine->adjustAudioMixingPlayoutVolume(pos); + m_rtcEngine->adjustAudioMixingPublishVolume(pos); + *pResult = 0; +} diff --git a/windows/APIExample/APIExample/Advanced/AudioMixing/CAgoraAudioMixingDlg.h b/windows/APIExample/APIExample/Advanced/AudioMixing/CAgoraAudioMixingDlg.h new file mode 100644 index 000000000..b64e9fadc --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/AudioMixing/CAgoraAudioMixingDlg.h @@ -0,0 +1,145 @@ +#pragma once +#include "AGVideoWnd.h" + + +class CAudioMixingEventHandler : public IRtcEngineEventHandler +{ +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override; + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override; + /** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. + */ + virtual void onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) override; +private: + HWND m_hMsgHanlder; +}; + + +class CAgoraAudioMixingDlg : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraAudioMixingDlg) + +public: + CAgoraAudioMixingDlg(CWnd* pParent = nullptr); + virtual ~CAgoraAudioMixingDlg(); + + enum { IDD = IDD_DIALOG_AUDIO_MIX }; + +public: + //Initialize the Ctrl Text. + void InitCtrlText(); + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //render local video from SDK local capture. + void RenderLocalVideo(); + //resume window status + void ResumeStatus(); + +protected: + virtual void DoDataExchange(CDataExchange* pDX); + DECLARE_MESSAGE_MAP() + LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam); +private: + bool m_joinChannel = false; + bool m_initialize = false; + bool m_audioMixing = false; + IRtcEngine* m_rtcEngine = nullptr; + CAGVideoWnd m_localVideoWnd; + CAudioMixingEventHandler m_eventHandler; + +public: + CStatic m_staVideoArea; + CListBox m_lstInfo; + CStatic m_staDetail; + CStatic m_staChannel; + CEdit m_edtChannel; + CButton m_btnJoinChannel; + CStatic m_staAudioMix; + CStatic m_staAudioRepeat; + CEdit m_edtAudioMix; + CButton m_btnSetAudioMix; + CEdit m_edtRepatTimes; + CButton m_chkOnlyLocal; + CButton m_chkMicroPhone; + afx_msg void OnSelchangeListInfoBroadcasting(); + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + virtual BOOL OnInitDialog(); + virtual BOOL PreTranslateMessage(MSG* pMsg); + afx_msg void OnBnClickedButtonJoinchannel(); + afx_msg void OnBnClickedButtonSetAudioMix(); + CStatic m_staVolume; + CSliderCtrl m_sldVolume; + afx_msg void OnReleasedcaptureSliderVolume(NMHDR *pNMHDR, LRESULT *pResult); +}; diff --git a/windows/APIExample/APIExample/Advanced/AudioProfile/CAgoraAudioProfile.cpp b/windows/APIExample/APIExample/Advanced/AudioProfile/CAgoraAudioProfile.cpp new file mode 100644 index 000000000..1d61cf69c --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/AudioProfile/CAgoraAudioProfile.cpp @@ -0,0 +1,464 @@ +#include "stdafx.h" +#include "APIExample.h" +#include "CAgoraAudioProfile.h" + + + +IMPLEMENT_DYNAMIC(CAgoraAudioProfile, CDialogEx) + +CAgoraAudioProfile::CAgoraAudioProfile(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_AUDIO_PROFILE, pParent) +{ + +} + +CAgoraAudioProfile::~CAgoraAudioProfile() +{ +} + +void CAgoraAudioProfile::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannel); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannel); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_STATIC_ADUIO_PROFILE, m_staAudioProfile); + DDX_Control(pDX, IDC_STATIC_ADUIO_SCENARIO, m_staAudioScenario); + DDX_Control(pDX, IDC_COMBO_AUDIO_PROFILE, m_cmbAudioProfile); + DDX_Control(pDX, IDC_COMBO_AUDIO_SCENARIO, m_cmbAudioScenario); + DDX_Control(pDX, IDC_BUTTON_SET_AUDIO_PROFILE, m_btnSetAudioProfile); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); + DDX_Control(pDX, IDC_STATIC_DETAIL, m_staDetail); +} + +//init ctrl text. +void CAgoraAudioProfile::InitCtrlText() +{ + m_staChannel.SetWindowText(commonCtrlChannel); + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + m_staAudioProfile.SetWindowText(audioProfileCtrlProfile); + m_staAudioScenario.SetWindowText(audioProfileCtrlScenario); + m_btnSetAudioProfile.SetWindowText(audioProfileCtrlSetAudioProfile); +} + +//Initialize the Agora SDK +bool CAgoraAudioProfile::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //enable video in the engine. + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + return true; +} + +void CAgoraAudioProfile::UnInitAgora() +{ + if (m_rtcEngine) { + if (m_joinChannel) + //leave channel + m_joinChannel = !m_rtcEngine->leaveChannel(); + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} + + +//render local video from SDK local capture. +void CAgoraAudioProfile::RenderLocalVideo() +{ + if (m_rtcEngine) { + //start preview in the engine. + m_rtcEngine->startPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("startPreview")); + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_localVideoWnd.GetSafeHwnd(); + //setup local video in the engine to canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setupLocalVideo")); + } +} + +//resume status. +void CAgoraAudioProfile::ResumeStatus() +{ + InitCtrlText(); + m_staDetail.SetWindowText(_T("")); + m_edtChannel.SetWindowText(_T("")); + m_cmbAudioProfile.SetCurSel(0); + m_cmbAudioScenario.SetCurSel(0); + m_btnSetAudioProfile.EnableWindow(TRUE); + m_lstInfo.ResetContent(); + m_joinChannel = false; + m_initialize = false; + m_setAudio = false; +} + + +BEGIN_MESSAGE_MAP(CAgoraAudioProfile, CDialogEx) + ON_WM_SHOWWINDOW() + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraAudioProfile::OnBnClickedButtonJoinchannel) + ON_BN_CLICKED(IDC_BUTTON_SET_AUDIO_PROFILE, &CAgoraAudioProfile::OnBnClickedButtonSetAudioProfile) + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraAudioProfile::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraAudioProfile::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraAudioProfile::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraAudioProfile::OnEIDUserOffline) + ON_MESSAGE(WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), &CAgoraAudioProfile::OnEIDRemoteVideoStateChanged) + ON_LBN_SELCHANGE(IDC_LIST_INFO_BROADCASTING, &CAgoraAudioProfile::OnSelchangeListInfoBroadcasting) +END_MESSAGE_MAP() + + + +void CAgoraAudioProfile::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow)//bShwo is true ,show window + { + InitCtrlText(); + RenderLocalVideo(); + } + else { + ResumeStatus(); + } +} + + +BOOL CAgoraAudioProfile::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + m_localVideoWnd.MoveWindow(&rcArea); + m_localVideoWnd.ShowWindow(SW_SHOW); + + int nIndex = 0; + m_cmbAudioProfile.InsertString(nIndex++, _T("AUDIO_PROFILE_DEFAULT")); + m_cmbAudioProfile.InsertString(nIndex++, _T("AUDIO_PROFILE_SPEECH_STANDARD")); + m_cmbAudioProfile.InsertString(nIndex++, _T("AUDIO_PROFILE_MUSIC_STANDARD")); + m_cmbAudioProfile.InsertString(nIndex++, _T("AUDIO_PROFILE_MUSIC_STANDARD_STEREO")); + m_cmbAudioProfile.InsertString(nIndex++, _T("AUDIO_PROFILE_MUSIC_HIGH_QUALITY")); + m_cmbAudioProfile.InsertString(nIndex++, _T("AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO")); + m_cmbAudioProfile.InsertString(nIndex++, _T("AUDIO_PROFILE_IOT")); + + nIndex = 0; + m_cmbAudioScenario.InsertString(nIndex++, _T("AUDIO_SCENARIO_DEFAULT")); + m_cmbAudioScenario.InsertString(nIndex++, _T("AUDIO_SCENARIO_CHATROOM_ENTERTAINMENT")); + m_cmbAudioScenario.InsertString(nIndex++, _T("AUDIO_SCENARIO_EDUCATION")); + m_cmbAudioScenario.InsertString(nIndex++, _T("AUDIO_SCENARIO_GAME_STREAMING")); + m_cmbAudioScenario.InsertString(nIndex++, _T("AUDIO_SCENARIO_SHOWROOM")); + m_cmbAudioScenario.InsertString(nIndex++, _T("AUDIO_SCENARIO_CHATROOM_GAMING")); + m_cmbAudioScenario.InsertString(nIndex++, _T("AUDIO_SCENARIO_IOT")); + m_cmbAudioScenario.InsertString(8, _T("AUDIO_SCENARIO_MEETING")); + + + ResumeStatus(); + + return TRUE; +} + + +void CAgoraAudioProfile::OnBnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + CString strInfo; + if (!m_joinChannel) { + CString strChannelName; + m_edtChannel.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + std::string szChannelId = cs2utf8(strChannelName); + //join channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + strInfo.Format(_T("join channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + } + } + else { + //leave channel in the engine. + if (0 == m_rtcEngine->leaveChannel()) { + strInfo.Format(_T("leave channel %s"), getCurrentTime()); + } + } + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + +// set audio profile or unset. +void CAgoraAudioProfile::OnBnClickedButtonSetAudioProfile() +{ + if (!m_setAudio) + { + int nProfileSel = m_cmbAudioProfile.GetCurSel(); + int nScenSel = m_cmbAudioScenario.GetCurSel(); + CString strInfo; + CString strAudioProfile; + CString strAudioScenario; + m_cmbAudioProfile.GetWindowText(strAudioProfile); + m_cmbAudioScenario.GetWindowText(strAudioScenario); + strInfo.Format(_T("Profile:%s,\nScenario:%s"), strAudioProfile, strAudioScenario); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + //set audio profile. + m_rtcEngine->setAudioProfile((AUDIO_PROFILE_TYPE)nProfileSel, (AUDIO_SCENARIO_TYPE)nScenSel); + m_btnSetAudioProfile.SetWindowText(audioProfileCtrlUnSetAudioProfile); + }else{ + m_rtcEngine->setAudioProfile((AUDIO_PROFILE_TYPE)0, (AUDIO_SCENARIO_TYPE)0); + m_btnSetAudioProfile.SetWindowText(audioProfileCtrlSetAudioProfile); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("reset default audio profile")); + } + m_setAudio = !m_setAudio; +} + + + + + + +//EID_JOINCHANNEL_SUCCESS message window handler +LRESULT CAgoraAudioProfile::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = true; + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + m_btnJoinChannel.EnableWindow(TRUE); + m_btnSetAudioProfile.EnableWindow(FALSE); + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_localVideoWnd.SetUID(wParam); + //notify parent window + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); + return 0; +} + +//EID_LEAVEHANNEL_SUCCESS message window handler +LRESULT CAgoraAudioProfile::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + m_btnSetAudioProfile.EnableWindow(TRUE); + CString strInfo; + strInfo.Format(_T("leave channel success %s"), getCurrentTime()); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return 0; +} + +//EID_USER_JOINED message window handler +LRESULT CAgoraAudioProfile::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + CString strInfo; + strInfo.Format(_T("%u joined"), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + + return 0; +} + +//EID_USER_OFFLINE message handler. +LRESULT CAgoraAudioProfile::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + m_rtcEngine->setupRemoteVideo(canvas); + CString strInfo; + strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + +//EID_REMOTE_VIDEO_STATE_CHANED message window handler. +LRESULT CAgoraAudioProfile::OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam) +{ + PVideoStateStateChanged stateChanged = (PVideoStateStateChanged)wParam; + if (stateChanged) { + //onRemoteVideoStateChanged + CString strSateInfo; + switch (stateChanged->state) { + case REMOTE_VIDEO_STATE_STARTING: + strSateInfo = _T("REMOTE_VIDEO_STATE_STARTING"); + break; + case REMOTE_VIDEO_STATE_STOPPED: + strSateInfo = _T("strSateInfo"); + break; + case REMOTE_VIDEO_STATE_DECODING: + strSateInfo = _T("REMOTE_VIDEO_STATE_DECODING"); + break; + case REMOTE_VIDEO_STATE_FAILED: + strSateInfo = _T("REMOTE_VIDEO_STATE_FAILED "); + break; + case REMOTE_VIDEO_STATE_FROZEN: + strSateInfo = _T("REMOTE_VIDEO_STATE_FROZEN "); + break; + } + CString strInfo; + strInfo.Format(_T("onRemoteVideoStateChanged: uid=%u, %s"), stateChanged->uid, strSateInfo); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + return 0; +} + + + +/* +note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one +parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). +*/ +void CAudioProfileEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } +} +/* +note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. +parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). +*/ +void CAudioProfileEventHandler::onUserJoined(uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } +} + +/* +note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. +parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. +*/ +void CAudioProfileEventHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } +} +/* +note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. +parameters: + stats: Call statistics. +*/ + +void CAudioProfileEventHandler::onLeaveChannel(const RtcStats& stats) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } +} +/** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. +*/ +void CAudioProfileEventHandler::onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) +{ + if (m_hMsgHanlder) { + PVideoStateStateChanged stateChanged = new VideoStateStateChanged; + stateChanged->uid = uid; + stateChanged->reason = reason; + stateChanged->state = state; + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), (WPARAM)stateChanged, 0); + } +} + + + + +BOOL CAgoraAudioProfile::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} + + +void CAgoraAudioProfile::OnSelchangeListInfoBroadcasting() +{ + int sel = m_lstInfo.GetCurSel(); + if (sel < 0)return; + CString strDetail; + m_lstInfo.GetText(sel, strDetail); + m_staDetail.SetWindowText(strDetail); +} diff --git a/windows/APIExample/APIExample/Advanced/AudioProfile/CAgoraAudioProfile.h b/windows/APIExample/APIExample/Advanced/AudioProfile/CAgoraAudioProfile.h new file mode 100644 index 000000000..4bc29b3fd --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/AudioProfile/CAgoraAudioProfile.h @@ -0,0 +1,144 @@ +#pragma once +#include "AGVideoWnd.h" + + +class CAudioProfileEventHandler : public IRtcEngineEventHandler +{ +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override; + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override; + /** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. + */ + virtual void onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) override; +private: + HWND m_hMsgHanlder; +}; + + +class CAgoraAudioProfile : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraAudioProfile) + +public: + CAgoraAudioProfile(CWnd* pParent = nullptr); + virtual ~CAgoraAudioProfile(); + + enum { IDD = IDD_DIALOG_AUDIO_PROFILE }; + +protected: + virtual void DoDataExchange(CDataExchange* pDX); + +public: + //Initialize the Ctrl Text. + void InitCtrlText(); + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //render local video from SDK local capture. + void RenderLocalVideo(); + //resume window status + void ResumeStatus(); + +private: + bool m_joinChannel = false; + bool m_initialize = false; + bool m_setAudio = false; + IRtcEngine* m_rtcEngine = nullptr; + CAGVideoWnd m_localVideoWnd; + CAudioProfileEventHandler m_eventHandler; +public: + + LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam); + + DECLARE_MESSAGE_MAP() + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + virtual BOOL OnInitDialog(); + virtual BOOL PreTranslateMessage(MSG* pMsg); + afx_msg void OnBnClickedButtonJoinchannel(); + afx_msg void OnBnClickedButtonSetAudioProfile(); + afx_msg void OnSelchangeListInfoBroadcasting(); + +public: + CStatic m_staVideoArea; + CStatic m_staChannel; + CEdit m_edtChannel; + CButton m_btnJoinChannel; + CStatic m_staAudioProfile; + CStatic m_staAudioScenario; + CComboBox m_cmbAudioProfile; + CComboBox m_cmbAudioScenario; + CButton m_btnSetAudioProfile; + CListBox m_lstInfo; + CStatic m_staDetail; +}; diff --git a/windows/APIExample/APIExample/Advanced/AudioVolume/CAgoraAudioVolumeDlg.cpp b/windows/APIExample/APIExample/Advanced/AudioVolume/CAgoraAudioVolumeDlg.cpp new file mode 100644 index 000000000..738134749 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/AudioVolume/CAgoraAudioVolumeDlg.cpp @@ -0,0 +1,513 @@ +#include "stdafx.h" +#include "APIExample.h" +#include "CAgoraAudioVolumeDlg.h" + + + +IMPLEMENT_DYNAMIC(CAgoraAudioVolumeDlg, CDialogEx) + +CAgoraAudioVolumeDlg::CAgoraAudioVolumeDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_VOLUME, pParent) +{ + +} + +CAgoraAudioVolumeDlg::~CAgoraAudioVolumeDlg() +{ +} + + + +void CAgoraAudioVolumeDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannel); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannel); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_STATIC_AUDIO_CAP_VOL, m_staCapVol); + DDX_Control(pDX, IDC_SLIDER_CAP_VOLUME, m_sldCapVol); + DDX_Control(pDX, IDC_STATIC_AUDIO_SIGNAL_VOL, m_staCapSigVol); + DDX_Control(pDX, IDC_SLIDER_SIGNAL_VOLUME2, m_sldCapSigVol); + DDX_Control(pDX, IDC_STATIC_PLAYBACK_VOL, m_staPlaybackVol); + DDX_Control(pDX, IDC_SLIDER_PLAYBACK_VOLUME, m_sldPlaybackVol); + DDX_Control(pDX, IDC_STATIC_PLAYBACK_VOL_SIGNAL, m_staPlaybackSigVol); + DDX_Control(pDX, IDC_SLIDER_PLAYBACK_SIGNAL_VOLUME, m_sldPlaybackSigVol); + DDX_Control(pDX, IDC_STATIC_DETAIL, m_details); + DDX_Control(pDX, IDC_STATIC_SPEAKER_INFO, m_staSpeaker_Info); +} + +//init ctrl text. +void CAgoraAudioVolumeDlg::InitCtrlText() +{ + m_staCapSigVol.SetWindowText(AudioVolumeCtrlCapSigVol); + m_staCapVol.SetWindowText(AudioVolumeCtrlCapVol); + m_staPlaybackVol.SetWindowText(AudioVolumeCtrlPlaybackVol); + m_staPlaybackSigVol.SetWindowText(AudioVolumeCtrlPlaybackSigVol); + m_staChannel.SetWindowText(commonCtrlChannel); + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); +} + +//Initialize the Agora SDK +bool CAgoraAudioVolumeDlg::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_audioDeviceManager = new AAudioDeviceManager(m_rtcEngine); + m_rtcEngine->enableAudioVolumeIndication(1000, 0, true); + int vol; + m_audioDeviceManager->get()->getRecordingDeviceVolume(&vol); + m_sldCapVol.SetPos(vol); + m_audioDeviceManager->get()->getPlaybackDeviceVolume(&vol); + m_sldPlaybackVol.SetPos(vol); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //enable video in the engine. + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + return true; +} + +void CAgoraAudioVolumeDlg::UnInitAgora() +{ + if (m_rtcEngine) { + if (m_joinChannel) + //leave channel + m_joinChannel = !m_rtcEngine->leaveChannel(); + delete m_audioDeviceManager; + if (m_audioDeviceManager) + { + m_audioDeviceManager->release(); + } + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} + + +//render local video from SDK local capture. +void CAgoraAudioVolumeDlg::RenderLocalVideo() +{ + if (m_rtcEngine) { + //start preview in the engine. + m_rtcEngine->startPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("startPreview")); + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_localVideoWnd.GetSafeHwnd(); + //setup local video in the engine to canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setupLocalVideo")); + } +} + +//resume status. +void CAgoraAudioVolumeDlg::ResumeStatus() +{ + if (m_audioIndiaction) + { + delete []m_audioIndiaction->speakers; + delete m_audioIndiaction; + m_audioIndiaction = nullptr; + } + if (m_activeSpeackerUid) { + delete m_activeSpeackerUid; + m_activeSpeackerUid = nullptr; + } + InitCtrlText(); + m_staSpeaker_Info.SetWindowText(_T("")); + m_edtChannel.SetWindowText(_T("")); + m_lstInfo.ResetContent(); + m_joinChannel = false; + m_initialize = false; +} + +BEGIN_MESSAGE_MAP(CAgoraAudioVolumeDlg, CDialogEx) + ON_WM_SHOWWINDOW() + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraAudioVolumeDlg::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraAudioVolumeDlg::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraAudioVolumeDlg::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraAudioVolumeDlg::OnEIDUserOffline) + ON_MESSAGE(WM_MSGID(EID_AUDIO_VOLUME_INDICATION), &CAgoraAudioVolumeDlg::OnEIDAudioVolumeIndication) + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraAudioVolumeDlg::OnBnClickedButtonJoinchannel) + ON_LBN_SELCHANGE(IDC_LIST_INFO_BROADCASTING, &CAgoraAudioVolumeDlg::OnSelchangeListInfoBroadcasting) + ON_NOTIFY(NM_RELEASEDCAPTURE, IDC_SLIDER_CAP_VOLUME, &CAgoraAudioVolumeDlg::OnReleasedcaptureSliderCapVolume) + ON_NOTIFY(NM_RELEASEDCAPTURE, IDC_SLIDER_SIGNAL_VOLUME2, &CAgoraAudioVolumeDlg::OnReleasedcaptureSliderSignalVolume2) + ON_NOTIFY(NM_RELEASEDCAPTURE, IDC_SLIDER_PLAYBACK_VOLUME, &CAgoraAudioVolumeDlg::OnReleasedcaptureSliderPlaybackVolume) + ON_NOTIFY(NM_RELEASEDCAPTURE, IDC_SLIDER_PLAYBACK_SIGNAL_VOLUME, &CAgoraAudioVolumeDlg::OnReleasedcaptureSliderPlaybackSignalVolume) + ON_WM_TIMER() +END_MESSAGE_MAP() + + + + +void CAgoraAudioVolumeDlg::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow)//bShwo is true ,show window + { + InitCtrlText(); + RenderLocalVideo(); + } + else { + ResumeStatus(); + } + +} + + +BOOL CAgoraAudioVolumeDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + m_localVideoWnd.MoveWindow(&rcArea); + m_localVideoWnd.ShowWindow(SW_SHOW); + + m_sldCapVol.SetRange(0, 255); + m_sldCapSigVol.SetRange(0, 400); + m_sldPlaybackVol.SetRange(0, 255); + m_sldPlaybackSigVol.SetRange(0, 400); + + ResumeStatus(); + return TRUE; +} + + +BOOL CAgoraAudioVolumeDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} + + +void CAgoraAudioVolumeDlg::OnBnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + CString strInfo; + if (!m_joinChannel) { + CString strChannelName; + m_edtChannel.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + std::string szChannelId = cs2utf8(strChannelName); + //join channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + strInfo.Format(_T("join channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + m_staSpeaker_Info.SetWindowText(_T("")); + SetTimer(1001, 1000, NULL); + } + } + else { + //leave channel in the engine. + if (0 == m_rtcEngine->leaveChannel()) { + strInfo.Format(_T("leave channel %s"), getCurrentTime()); + KillTimer(1001); + m_staSpeaker_Info.SetWindowText(_T("")); + } + } + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + + +void CAgoraAudioVolumeDlg::OnSelchangeListInfoBroadcasting() +{ + int sel = m_lstInfo.GetCurSel(); + if (sel < 0)return; + CString strDetail; + m_lstInfo.GetText(sel, strDetail); + m_details.SetWindowText(strDetail); +} + + +void CAgoraAudioVolumeDlg::OnReleasedcaptureSliderCapVolume(NMHDR *pNMHDR, LRESULT *pResult) +{ + + LPNMCUSTOMDRAW pNMCD = reinterpret_cast(pNMHDR); + int vol = m_sldCapVol.GetPos(); + (*m_audioDeviceManager)->setRecordingDeviceVolume(vol); + *pResult = 0; +} + + +void CAgoraAudioVolumeDlg::OnReleasedcaptureSliderSignalVolume2(NMHDR *pNMHDR, LRESULT *pResult) +{ + LPNMCUSTOMDRAW pNMCD = reinterpret_cast(pNMHDR); + int vol = m_sldCapVol.GetPos(); + m_rtcEngine->adjustRecordingSignalVolume(vol); + *pResult = 0; +} + + +void CAgoraAudioVolumeDlg::OnReleasedcaptureSliderPlaybackVolume(NMHDR *pNMHDR, LRESULT *pResult) +{ + LPNMCUSTOMDRAW pNMCD = reinterpret_cast(pNMHDR); + int vol = m_sldCapVol.GetPos(); + (*m_audioDeviceManager)->setPlaybackDeviceVolume(vol); + *pResult = 0; +} + + +void CAgoraAudioVolumeDlg::OnReleasedcaptureSliderPlaybackSignalVolume(NMHDR *pNMHDR, LRESULT *pResult) +{ + LPNMCUSTOMDRAW pNMCD = reinterpret_cast(pNMHDR); + int vol = m_sldCapVol.GetPos(); + m_rtcEngine->adjustPlaybackSignalVolume(vol); + *pResult = 0; +} + + +LRESULT CAgoraAudioVolumeDlg::OnEIDAudioVolumeIndication(WPARAM wparam, LPARAM lparam) +{ + if (m_audioIndiaction) { + delete m_audioIndiaction; + m_audioIndiaction = nullptr; + } + m_audioIndiaction = reinterpret_cast(wparam); + return TRUE; +} + +LRESULT CAgoraAudioVolumeDlg::OnEIDActiveSpeaker(WPARAM wparam, LPARAM lparam) +{ + if (m_activeSpeackerUid) + { + delete m_activeSpeackerUid; + m_activeSpeackerUid = new uid_t(wparam); + } + return TRUE; +} + + +//audio volume indication +void CAudioVolumeEventHandler::onAudioVolumeIndication(const AudioVolumeInfo * speakers, unsigned int speakerNumber, int totalVolume) +{ + auto p = new AudioIndication; + p->speakerNumber = speakerNumber; + p->speakers = new AudioVolumeInfo[speakerNumber]; + for (unsigned int i = 0; i < speakerNumber; i++) + p->speakers[i] = speakers[i]; + if (m_hMsgHanlder) + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_AUDIO_VOLUME_INDICATION), (WPARAM)p, 0); +} + +//active speaker +void CAudioVolumeEventHandler::onActiveSpeaker(uid_t uid) +{ + if (m_hMsgHanlder) + { + ::PostMessage(m_hMsgHanlder,WM_MSGID(EID_AUDIO_ACTIVE_SPEAKER), uid,0); + } + +} + +//EID_JOINCHANNEL_SUCCESS message window handler +LRESULT CAgoraAudioVolumeDlg::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = true; + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + m_btnJoinChannel.EnableWindow(TRUE); + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_localVideoWnd.SetUID(wParam); + //notify parent window + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); + return 0; +} + +//EID_LEAVEHANNEL_SUCCESS message window handler +LRESULT CAgoraAudioVolumeDlg::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + CString strInfo; + strInfo.Format(_T("leave channel success %s"), getCurrentTime()); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return 0; +} + +//EID_USER_JOINED message window handler +LRESULT CAgoraAudioVolumeDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + CString strInfo; + strInfo.Format(_T("%u joined"), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + + return 0; +} + +//EID_USER_OFFLINE message handler. +LRESULT CAgoraAudioVolumeDlg::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + m_rtcEngine->setupRemoteVideo(canvas); + CString strInfo; + strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + + + + + +/* +note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one +parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). +*/ +void CAudioVolumeEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } +} +/* +note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. +parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). +*/ +void CAudioVolumeEventHandler::onUserJoined(uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } +} + +/* +note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. +parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. +*/ +void CAudioVolumeEventHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } +} +/* +note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. +parameters: + stats: Call statistics. +*/ + +void CAudioVolumeEventHandler::onLeaveChannel(const RtcStats& stats) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } +} + + + +// show speakers +void CAgoraAudioVolumeDlg::OnTimer(UINT_PTR nIDEvent) +{ + if (nIDEvent == 1001) + { + CString strInfo; + if (m_audioIndiaction) + { + strInfo = _T("speaks["); + for (unsigned i = 0; i < m_audioIndiaction->speakerNumber; i++) + { + CString tmp; + tmp.Format(_T("%d,"), m_audioIndiaction->speakers[i].uid); + if (i == m_audioIndiaction->speakerNumber - 1) + { + tmp.Format(_T("%d"), m_audioIndiaction->speakers[i].uid); + } + strInfo += tmp; + } + strInfo += _T("]"); + } + if (m_activeSpeackerUid) + { + CString tmp; + tmp.Format(_T("active speacker uid:%d"), *m_activeSpeackerUid); + strInfo += tmp; + } + m_staSpeaker_Info.SetWindowText(strInfo); + return; + } + CDialogEx::OnTimer(nIDEvent); +} diff --git a/windows/APIExample/APIExample/Advanced/AudioVolume/CAgoraAudioVolumeDlg.h b/windows/APIExample/APIExample/Advanced/AudioVolume/CAgoraAudioVolumeDlg.h new file mode 100644 index 000000000..2caef422a --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/AudioVolume/CAgoraAudioVolumeDlg.h @@ -0,0 +1,183 @@ +#pragma once +#include "AGVideoWnd.h" +struct AudioIndication +{ + AudioVolumeInfo * speakers; + unsigned int speakerNumber; + int totalVolume; +}; + +class CAudioVolumeEventHandler : public IRtcEngineEventHandler +{ +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override; + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override; + /** + Reports which users are speaking, the speakers' volume and whether the local user is speaking. + This callback reports the IDs and volumes of the loudest speakers (at most 3 users) at the moment in the channel, and whether the local user is speaking. + By default, this callback is disabled. You can enable it by calling the \ref IRtcEngine::enableAudioVolumeIndication(int, int, bool) "enableAudioVolumeIndication" method. + Once enabled, this callback is triggered at the set interval, regardless of whether a user speaks or not. + The SDK triggers two independent `onAudioVolumeIndication` callbacks at one time, which separately report the volume information of the local user and all the remote speakers. + For more information, see the detailed parameter descriptions. + @note + - To enable the voice activity detection of the local user, ensure that you set `report_vad`(true) in the `enableAudioVolumeIndication` method. + - Calling the \ref agora::rtc::IRtcEngine::muteLocalAudioStream "muteLocalAudioStream" method affects the SDK's behavior: + - If the local user calls the \ref agora::rtc::IRtcEngine::muteLocalAudioStream "muteLocalAudioStream" method, the SDK stops triggering the local user's callback. + - 20 seconds after a remote speaker calls the *muteLocalAudioStream* method, the remote speakers' callback excludes this remote user's information; 20 seconds after all remote users call the *muteLocalAudioStream* method, the SDK stops triggering the remote speakers' callback. + - An empty @p speakers array in the *onAudioVolumeIndication* callback suggests that no remote user is speaking at the moment. + @param speakers A pointer to AudioVolumeInfo: + - In the local user's callback, this struct contains the following members: + - `uid` = 0, + - `volume` = `totalVolume`, which reports the sum of the voice volume and audio-mixing volume of the local user, and + - `vad`, which reports the voice activity status of the local user. + - In the remote speakers' callback, this array contains the following members: + - `uid` of the remote speaker, + - `volume`, which reports the sum of the voice volume and audio-mixing volume of each remote speaker, and + - `vad` = 0. + An empty speakers array in the callback indicates that no remote user is speaking at the moment. + @param speakerNumber Total number of speakers. The value range is [0, 3]. + - In the local user's callback, `speakerNumber` = 1, regardless of whether the local user speaks or not. + - In the remote speakers' callback, the callback reports the IDs and volumes of the three loudest speakers when there are more than three remote users in the channel, and `speakerNumber` = 3. + @param totalVolume Total volume after audio mixing. The value ranges between 0 (lowest volume) and 255 (highest volume). + - In the local user's callback, `totalVolume` is the sum of the voice volume and audio-mixing volume of the local user. + - In the remote speakers' callback, `totalVolume` is the sum of the voice volume and audio-mixing volume of all the remote speakers. + */ + virtual void onAudioVolumeIndication(const AudioVolumeInfo* speakers, unsigned int speakerNumber, int totalVolume) override; + /** + Reports which user is the loudest speaker. + If the user enables the audio volume indication by calling the \ref IRtcEngine::enableAudioVolumeIndication(int, int, bool) "enableAudioVolumeIndication" method, this callback returns the @p uid of the active speaker detected by the audio volume detection module of the SDK. + @note + - To receive this callback, you need to call the \ref IRtcEngine::enableAudioVolumeIndication(int, int, bool) "enableAudioVolumeIndication" method. + - This callback returns the user ID of the user with the highest voice volume during a period of time, instead of at the moment. + @param uid User ID of the active speaker. A @p uid of 0 represents the local user. + */ + virtual void onActiveSpeaker(uid_t uid) override; +private: + HWND m_hMsgHanlder; +}; + +class CAgoraAudioVolumeDlg : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraAudioVolumeDlg) + +public: + CAgoraAudioVolumeDlg(CWnd* pParent = nullptr); + virtual ~CAgoraAudioVolumeDlg(); + + enum { IDD = IDD_DIALOG_VOLUME }; +public: + //Initialize the Ctrl Text. + void InitCtrlText(); + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //render local video from SDK local capture. + void RenderLocalVideo(); + //resume window status + void ResumeStatus(); + +private: + bool m_joinChannel = false; + bool m_initialize = false; + IRtcEngine* m_rtcEngine = nullptr; + CAGVideoWnd m_localVideoWnd; + CAudioVolumeEventHandler m_eventHandler; + AudioIndication *m_audioIndiaction = nullptr; + AAudioDeviceManager *m_audioDeviceManager = nullptr; + uid_t *m_activeSpeackerUid = nullptr; + +protected: + LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDAudioVolumeIndication(WPARAM wparam, LPARAM lparam); + LRESULT OnEIDActiveSpeaker(WPARAM wparam, LPARAM lparam); + + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + virtual BOOL OnInitDialog(); + virtual BOOL PreTranslateMessage(MSG* pMsg); + afx_msg void OnBnClickedButtonJoinchannel(); + afx_msg void OnSelchangeListInfoBroadcasting(); + afx_msg void OnReleasedcaptureSliderCapVolume(NMHDR *pNMHDR, LRESULT *pResult); + afx_msg void OnReleasedcaptureSliderSignalVolume2(NMHDR *pNMHDR, LRESULT *pResult); + afx_msg void OnReleasedcaptureSliderPlaybackVolume(NMHDR *pNMHDR, LRESULT *pResult); + afx_msg void OnReleasedcaptureSliderPlaybackSignalVolume(NMHDR *pNMHDR, LRESULT *pResult); + virtual void DoDataExchange(CDataExchange* pDX); + + DECLARE_MESSAGE_MAP() +public: + CStatic m_staVideoArea; + CListBox m_lstInfo; + CStatic m_staChannel; + CEdit m_edtChannel; + CButton m_btnJoinChannel; + CStatic m_staCapVol; + CSliderCtrl m_sldCapVol; + CStatic m_staCapSigVol; + CSliderCtrl m_sldCapSigVol; + CStatic m_staPlaybackVol; + CSliderCtrl m_sldPlaybackVol; + CStatic m_staPlaybackSigVol; + CSliderCtrl m_sldPlaybackSigVol; + CStatic m_details; + + CStatic m_staSpeaker_Info; + afx_msg void OnTimer(UINT_PTR nIDEvent); +}; diff --git a/windows/APIExample/APIExample/Advanced/Beauty/CAgoraBeautyDlg.cpp b/windows/APIExample/APIExample/Advanced/Beauty/CAgoraBeautyDlg.cpp new file mode 100644 index 000000000..cb8ef1ea2 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/Beauty/CAgoraBeautyDlg.cpp @@ -0,0 +1,487 @@ +#include "stdafx.h" +#include "APIExample.h" +#include "CAgoraBeautyDlg.h" + + +IMPLEMENT_DYNAMIC(CAgoraBeautyDlg, CDialogEx) + +CAgoraBeautyDlg::CAgoraBeautyDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_BEAUTY, pParent) +{ + +} + +CAgoraBeautyDlg::~CAgoraBeautyDlg() +{ +} + +//Initialize the ctrl text. +void CAgoraBeautyDlg::InitCtrlText() +{ + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + m_staChannel.SetWindowText(commonCtrlChannel); + m_staRedness.SetWindowText(beautyCtrlRedness); + m_staLight.SetWindowText(beautyCtrlLightening); + m_staLightContrast.SetWindowText(beautyCtrlLighteningContrastLevel); + m_staSoomthness.SetWindowText(beautyCtrlSmoothness); + m_chkBeauty.SetWindowText(beautyCtrlEnable); +} + +//Initialize the Agora SDK +bool CAgoraBeautyDlg::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //enable video in the engine. + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + return true; +} + + +//UnInitialize the Agora SDK +void CAgoraBeautyDlg::UnInitAgora() +{ + if (m_rtcEngine) { + if (m_joinChannel) + //leave channel + m_joinChannel = !m_rtcEngine->leaveChannel(); + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} + +//render local video from SDK local capture. +void CAgoraBeautyDlg::RenderLocalVideo() +{ + if (m_rtcEngine) { + //start preview in the engine. + m_rtcEngine->startPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("startPreview")); + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_localVideoWnd.GetSafeHwnd(); + //setup local video in the engine to canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setupLocalVideo")); + + } +} + + +//resume window status +void CAgoraBeautyDlg::ResumeStatus() +{ + m_edtChannel.SetWindowText(_T("")); + m_edtLightLevel.SetWindowText(_T("")); + m_edtReadness.SetWindowText(_T("")); + m_edtSmoothness.SetWindowText(_T("")); + m_staDetail.SetWindowText(_T("")); + + m_chkBeauty.SetCheck(BST_UNCHECKED); + m_cmbBeautyLevel.SetCurSel(0); + m_lstInfo.ResetContent(); + SetBeauty(false); + m_joinChannel = false; + m_initialize = false; +} + + +void CAgoraBeautyDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannel); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannel); + DDX_Control(pDX, IDC_CHECK_BEAUTY_ENABLE, m_chkBeauty); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_COMBO_BEAUTE_LIGHTENING_CONTRAST_LEVEL, m_cmbBeautyLevel); + DDX_Control(pDX, IDC_EDIT_LIGHTENING, m_edtLightLevel); + DDX_Control(pDX, IDC_STATIC_BEAUTY_REDNESS, m_staRedness); + DDX_Control(pDX, IDC_STATIC_BEAUTY_SMOOTHNESS, m_staSoomthness); + DDX_Control(pDX, IDC_EDIT_BEAUTY_REDNESS, m_edtReadness); + DDX_Control(pDX, IDC_EDIT_BEAUTY_SMOOTHNESS, m_edtSmoothness); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); + DDX_Control(pDX, IDC_STATIC_BEAUTY_LIGHTENING_CONTRAST_LEVEL, m_staLightContrast); + DDX_Control(pDX, IDC_STATIC_BEAUTY_LIGHTENING, m_staLight); + DDX_Control(pDX, IDC_STATIC_DETAIL, m_staDetail); +} + + +BEGIN_MESSAGE_MAP(CAgoraBeautyDlg, CDialogEx) + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraBeautyDlg::OnBnClickedButtonJoinchannel) + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraBeautyDlg::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraBeautyDlg::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraBeautyDlg::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraBeautyDlg::OnEIDUserOffline) + ON_MESSAGE(WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), &CAgoraBeautyDlg::OnEIDRemoteVideoStateChanged) + ON_WM_SHOWWINDOW() + ON_BN_CLICKED(IDC_CHECK_BEAUTY_ENABLE, &CAgoraBeautyDlg::OnBnClickedCheckbeautyCtrlEnable) + ON_LBN_SELCHANGE(IDC_LIST_INFO_BROADCASTING, &CAgoraBeautyDlg::OnSelchangeListInfoBroadcasting) +END_MESSAGE_MAP() + + + +// join channel or level channel. +void CAgoraBeautyDlg::OnBnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + CString strInfo; + if (!m_joinChannel) { + CString strChannelName; + m_edtChannel.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + std::string szChannelId = cs2utf8(strChannelName); + //join channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + strInfo.Format(_T("join channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + } + } + else { + //leave channel in the engine. + if (0 == m_rtcEngine->leaveChannel()) { + strInfo.Format(_T("leave channel %s"), getCurrentTime()); + } + } + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + +} + + +// Set the lighteningContrastLevel,lighteningLevel,rednessLevel,smoothnessLevel. +void CAgoraBeautyDlg::SetBeauty(bool enabled, + agora::rtc::BeautyOptions::LIGHTENING_CONTRAST_LEVEL lighteningContrastLevel, + float lighteningLevel, + float rednessLevel, + float smoothnessLevel) +{ + //Beauty options to set + agora::rtc::BeautyOptions options; + options.lighteningContrastLevel = lighteningContrastLevel; + options.lighteningLevel = lighteningLevel; + options.smoothnessLevel = smoothnessLevel; + options.rednessLevel = rednessLevel; + //set Beauty options. + if(m_rtcEngine) + m_rtcEngine->setBeautyEffectOptions(enabled, options); +} + +//check box clicked to set beauty. +void CAgoraBeautyDlg::OnBnClickedCheckbeautyCtrlEnable() +{ + bool enabled = m_chkBeauty.GetCheck() == BST_CHECKED ? TRUE : FALSE; + //Beauty options to set + CString tmp; + auto lighteningContrastLevel = (agora::rtc::BeautyOptions::LIGHTENING_CONTRAST_LEVEL)m_cmbBeautyLevel.GetCurSel(); + float lighteningLevel; + float rednessLevel; + float smoothnessLevel; + m_edtLightLevel.GetWindowText(tmp); + auto func = [](float a)->float { + return a <0.0f ? 0.0f : a>1.0f ? 1.0f : a; + }; + lighteningLevel = func(static_cast(_ttof(tmp)/10)); + m_edtReadness.GetWindowText(tmp); + rednessLevel = func(static_cast(_ttof(tmp)/10)); + m_edtSmoothness.GetWindowText(tmp); + smoothnessLevel = func(static_cast(_ttof(tmp)/10)); + CString strInfo; + CString strlighteningContrastLevel; + m_cmbBeautyLevel.GetWindowText(strlighteningContrastLevel); + SetBeauty(enabled, lighteningContrastLevel, lighteningLevel, rednessLevel, smoothnessLevel); + if (enabled) + { + strInfo.Format(_T("lighteningContrastLevel:%s,\nlightening:%.1f,\nredness:%.1f,\nsmoothness:%.1f"), + strlighteningContrastLevel, + lighteningLevel, rednessLevel, smoothnessLevel); + } + else { + strInfo.Format(_T("unset beauty.")); + } + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + +//create views and init data. +BOOL CAgoraBeautyDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + m_localVideoWnd.MoveWindow(&rcArea); + m_localVideoWnd.ShowWindow(SW_SHOW); + + int nIndex = 0; + m_cmbBeautyLevel.InsertString(nIndex++, _T("Low contrast level")); + m_cmbBeautyLevel.InsertString(nIndex++, _T("Normal contrast level.")); + m_cmbBeautyLevel.InsertString(nIndex++, _T("High contrast level")); + + ResumeStatus(); + return TRUE; +} + + +//show window or hide window. +void CAgoraBeautyDlg::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow) { + InitCtrlText(); + RenderLocalVideo(); + } + else { + ResumeStatus(); + } +} + + + + +//EID_JOINCHANNEL_SUCCESS message window handler +LRESULT CAgoraBeautyDlg::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = true; + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + m_btnJoinChannel.EnableWindow(TRUE); + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_localVideoWnd.SetUID(wParam); + //notify parent window + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); + return 0; +} + +//EID_LEAVEHANNEL_SUCCESS message window handler +LRESULT CAgoraBeautyDlg::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + + CString strInfo; + strInfo.Format(_T("leave channel success %s"), getCurrentTime()); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return 0; +} + +//EID_USER_JOINED message window handler +LRESULT CAgoraBeautyDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + CString strInfo; + strInfo.Format(_T("%u joined"), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + + return 0; +} + +//EID_USER_OFFLINE message handler. +LRESULT CAgoraBeautyDlg::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + //set remote video to the engine. + m_rtcEngine->setupRemoteVideo(canvas); + CString strInfo; + strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + +//EID_REMOTE_VIDEO_STATE_CHANED message window handler. +LRESULT CAgoraBeautyDlg::OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam) +{ + PVideoStateStateChanged stateChanged = (PVideoStateStateChanged)wParam; + if (stateChanged) { + //onRemoteVideoStateChanged + CString strSateInfo; + switch (stateChanged->state) { + case REMOTE_VIDEO_STATE_STARTING: + strSateInfo = _T("REMOTE_VIDEO_STATE_STARTING"); + break; + case REMOTE_VIDEO_STATE_STOPPED: + strSateInfo = _T("strSateInfo"); + break; + case REMOTE_VIDEO_STATE_DECODING: + strSateInfo = _T("REMOTE_VIDEO_STATE_DECODING"); + break; + case REMOTE_VIDEO_STATE_FAILED: + strSateInfo = _T("REMOTE_VIDEO_STATE_FAILED "); + break; + case REMOTE_VIDEO_STATE_FROZEN: + strSateInfo = _T("REMOTE_VIDEO_STATE_FROZEN "); + break; + } + CString strInfo; + strInfo.Format(_T("onRemoteVideoStateChanged: uid=%u, %s"), stateChanged->uid, strSateInfo); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + return 0; +} + + + +/* +note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one +parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). +*/ +void CBeautyEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } +} +/* +note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. +parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). +*/ +void CBeautyEventHandler::onUserJoined(uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } +} + +/* +note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. +parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. +*/ +void CBeautyEventHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } +} +/* +note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. +parameters: + stats: Call statistics. +*/ + +void CBeautyEventHandler::onLeaveChannel(const RtcStats& stats) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } +} +/** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. +*/ +void CBeautyEventHandler::onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) +{ + if (m_hMsgHanlder) { + PVideoStateStateChanged stateChanged = new VideoStateStateChanged; + stateChanged->uid = uid; + stateChanged->reason = reason; + stateChanged->state = state; + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), (WPARAM)stateChanged, 0); + } +} + + + +BOOL CAgoraBeautyDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} + + +void CAgoraBeautyDlg::OnSelchangeListInfoBroadcasting() +{ + int sel = m_lstInfo.GetCurSel(); + if (sel < 0)return; + CString strDetail; + m_lstInfo.GetText(sel, strDetail); + m_staDetail.SetWindowText(strDetail); +} diff --git a/windows/APIExample/APIExample/Advanced/Beauty/CAgoraBeautyDlg.h b/windows/APIExample/APIExample/Advanced/Beauty/CAgoraBeautyDlg.h new file mode 100644 index 000000000..43a9490c7 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/Beauty/CAgoraBeautyDlg.h @@ -0,0 +1,151 @@ +#pragma once +#include "AGVideoWnd.h" + + +class CBeautyEventHandler : public IRtcEngineEventHandler +{ +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override; + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override; + /** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. + */ + virtual void onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) override; +private: + HWND m_hMsgHanlder; +}; + + +class CAgoraBeautyDlg : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraBeautyDlg) + +public: + CAgoraBeautyDlg(CWnd* pParent = nullptr); + virtual ~CAgoraBeautyDlg(); + enum { + IDD = IDD_DIALOG_BEAUTY + }; + +public: + //Initialize the ctrl text. + void InitCtrlText(); + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //render local video from SDK local capture. + void RenderLocalVideo(); + //resume window status + void ResumeStatus(); + //set Beauty + void SetBeauty(bool enabled, + agora::rtc::BeautyOptions::LIGHTENING_CONTRAST_LEVEL lighteningContrastLevel = + agora::rtc::BeautyOptions::LIGHTENING_CONTRAST_NORMAL, + float lighteningLevel = 0.0f, float rednessLevel = 0.0f, float smoothnessLevel = 0.0f); + bool m_joinChannel = false; + bool m_initialize = false; + IRtcEngine* m_rtcEngine = nullptr; + CAGVideoWnd m_localVideoWnd; + CBeautyEventHandler m_eventHandler; + +protected: + virtual void DoDataExchange(CDataExchange* pDX); + LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam); + + DECLARE_MESSAGE_MAP() +public: + CStatic m_staChannel; + CEdit m_edtChannel; + CButton m_chkBeauty; + CButton m_btnJoinChannel; + CComboBox m_cmbBeautyLevel; + CEdit m_edtLightLevel; + CStatic m_staRedness; + CStatic m_staSoomthness; + CEdit m_edtReadness; + CEdit m_edtSmoothness; + CStatic m_staVideoArea; + CListBox m_lstInfo; + CStatic m_staLightContrast; + CStatic m_staLight; + + afx_msg void OnBnClickedButtonJoinchannel(); + virtual BOOL OnInitDialog(); + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + afx_msg void OnBnClickedCheckbeautyCtrlEnable(); + virtual BOOL PreTranslateMessage(MSG* pMsg); + CStatic m_staDetail; + afx_msg void OnSelchangeListInfoBroadcasting(); +}; + + diff --git a/windows/APIExample/APIExample/Advanced/BeautyAudio/CAgoraBeautyAudio.cpp b/windows/APIExample/APIExample/Advanced/BeautyAudio/CAgoraBeautyAudio.cpp new file mode 100644 index 000000000..63c69e4cd --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/BeautyAudio/CAgoraBeautyAudio.cpp @@ -0,0 +1,558 @@ +#include "stdafx.h" +#include "APIExample.h" +#include "CAgoraBeautyAudio.h" + + + +IMPLEMENT_DYNAMIC(CAgoraBeautyAudio, CDialogEx) + +CAgoraBeautyAudio::CAgoraBeautyAudio(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_BEAUTY_AUDIO, pParent) +{ + +} + +CAgoraBeautyAudio::~CAgoraBeautyAudio() +{ +} + + +//Initialize the Ctrl Text. +void CAgoraBeautyAudio::InitCtrlText() +{ + m_staChannel.SetWindowText(commonCtrlChannel); + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + m_staAudioChange.SetWindowText(beautyAudioCtrlChange); + m_staAudioType.SetWindowText(beautyAudioCtrlPreSet); + m_btnSetBeautyAudio.SetWindowText(beautyAudioCtrlSetAudioChange); + m_staParam1.SetWindowText(beautyAudioCtrlParam1); + m_staParam2.SetWindowText(beautyAudioCtrlParam2); + +} + + + +//Initialize the Agora SDK +bool CAgoraBeautyAudio::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //enable video in the engine. + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + return true; +} + + +//UnInitialize the Agora SDK +void CAgoraBeautyAudio::UnInitAgora() +{ + if (m_rtcEngine) { + if (m_joinChannel) + //leave channel + m_joinChannel = !m_rtcEngine->leaveChannel(); + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} + +//render local video from SDK local capture. +void CAgoraBeautyAudio::RenderLocalVideo() +{ + if (m_rtcEngine) { + //start preview in the engine. + m_rtcEngine->startPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("startPreview")); + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_localVideoWnd.GetSafeHwnd(); + //setup local video in the engine to canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setupLocalVideo")); + } +} + + +//resume window status +void CAgoraBeautyAudio::ResumeStatus() +{ + InitCtrlText(); + m_staDetail.SetWindowText(_T("")); + m_edtChannel.SetWindowText(_T("")); + m_edtParam1.SetWindowText(_T("")); + m_edtParam2.SetWindowText(_T("")); + m_cmbAudioChange.SetCurSel(0); + m_btnSetBeautyAudio.SetWindowText(beautyAudioCtrlSetAudioChange); + OnSelchangeComboAudioChanger(); + m_lstInfo.ResetContent(); + m_joinChannel = false; + m_initialize = false; + m_beautyAudio = false; +} + +void CAgoraBeautyAudio::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannel); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannel); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_STATIC_AUDIO_CHANGER, m_staAudioChange); + DDX_Control(pDX, IDC_COMBO_AUDIO_CHANGER, m_cmbAudioChange); + DDX_Control(pDX, IDC_STATIC_DETAIL, m_staDetail); + DDX_Control(pDX, IDC_BUTTON_SET_BEAUTY_AUDIO, m_btnSetBeautyAudio); + DDX_Control(pDX, IDC_STATIC_BEAUTY_AUDIO_TYPE, m_staAudioType); + DDX_Control(pDX, IDC_COMBO_AUDIO_PERVERB_PRESET, m_cmbPerverbPreset); + DDX_Control(pDX, IDC_STATIC_PARAM1, m_staParam1); + DDX_Control(pDX, IDC_STATIC_PARAM2, m_staParam2); + DDX_Control(pDX, IDC_EDIT_PARAM1, m_edtParam1); + DDX_Control(pDX, IDC_EDIT_PARAM2, m_edtParam2); +} + + +BEGIN_MESSAGE_MAP(CAgoraBeautyAudio, CDialogEx) + ON_WM_SHOWWINDOW() + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraBeautyAudio::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraBeautyAudio::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraBeautyAudio::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraBeautyAudio::OnEIDUserOffline) + ON_MESSAGE(WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), &CAgoraBeautyAudio::OnEIDRemoteVideoStateChanged) + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraBeautyAudio::OnBnClickedButtonJoinchannel) + ON_BN_CLICKED(IDC_BUTTON_SET_BEAUTY_AUDIO, &CAgoraBeautyAudio::OnBnClickedButtonSetAudioChange) + ON_LBN_SELCHANGE(IDC_LIST_INFO_BROADCASTING, &CAgoraBeautyAudio::OnSelchangeListInfoBroadcasting) + ON_CBN_SELCHANGE(IDC_COMBO_AUDIO_CHANGER, &CAgoraBeautyAudio::OnSelchangeComboAudioChanger) +END_MESSAGE_MAP() + + + + +BOOL CAgoraBeautyAudio::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + m_localVideoWnd.MoveWindow(&rcArea); + m_localVideoWnd.ShowWindow(SW_SHOW); + + int nIndex = 0; + m_mapBeauty.insert( + std::make_pair(CString(_T("AudioEffect")), + std::vector({ + _T("AUDIO_EFFECT_OFF"), + _T("ROOM_ACOUSTICS_KTV") + ,_T("ROOM_ACOUSTICS_VOCAL_CONCERT") , + _T("ROOM_ACOUSTICS_STUDIO") , + _T("ROOM_ACOUSTICS_PHONOGRAPH") , + _T("ROOM_ACOUSTICS_VIRTUAL_STEREO") , + _T("ROOM_ACOUSTICS_SPACIAL"), + _T("ROOM_ACOUSTICS_ETHEREAL"), + _T("ROOM_ACOUSTICS_3D_VOICE"), + _T("VOICE_CHANGER_EFFECT_UNCLE"), + _T("VOICE_CHANGER_EFFECT_OLDMAN"), + _T("VOICE_CHANGER_EFFECT_BOY"), + _T("VOICE_CHANGER_EFFECT_SISTER"), + _T("VOICE_CHANGER_EFFECT_GIRL"), + _T("VOICE_CHANGER_EFFECT_PIGKING"), + _T("VOICE_CHANGER_EFFECT_HULK"), + _T("STYLE_TRANSFORMATION_RNB"), + _T("STYLE_TRANSFORMATION_POPULAR"), + _T("PITCH_CORRECTION"), }))); + + m_mapBeauty.insert( + std::make_pair(CString(_T("VoiceBeautifier")), + std::vector({ + _T("VOICE_BEAUTIFIER_OFF"), + _T("CHAT_BEAUTIFIER_MAGNETIC"), + _T("CHAT_BEAUTIFIER_FRESH"), + _T("CHAT_BEAUTIFIER_VITALITY"), + _T("TIMBRE_TRANSFORMATION_DEEP"), + _T("TIMBRE_TRANSFORMATION_MELLOW"), + _T("TIMBRE_TRANSFORMATION_FALSETTO"), + _T("TIMBRE_TRANSFORMATION_FULL"), + _T("TIMBRE_TRANSFORMATION_CLEAR"), + _T("TIMBRE_TRANSFORMATION_RESOUNDING"), + _T("TIMBRE_TRANSFORMATION_RINGING"), + }))); + + m_cmbAudioChange.InsertString(nIndex++, _T("AudioEffect")); + m_cmbAudioChange.InsertString(nIndex++, _T("VoiceBeautifier")); + + + m_setChanger.insert(std::make_pair(_T("AUDIO_EFFECT_OFF"), AUDIO_EFFECT_OFF)); + m_setChanger.insert(std::make_pair(_T("ROOM_ACOUSTICS_KTV"), ROOM_ACOUSTICS_KTV)); + m_setChanger.insert(std::make_pair(_T("ROOM_ACOUSTICS_VOCAL_CONCERT"), ROOM_ACOUSTICS_VOCAL_CONCERT)); + m_setChanger.insert(std::make_pair(_T("ROOM_ACOUSTICS_STUDIO"), ROOM_ACOUSTICS_STUDIO)); + m_setChanger.insert(std::make_pair(_T("ROOM_ACOUSTICS_PHONOGRAPH"), ROOM_ACOUSTICS_PHONOGRAPH)); + m_setChanger.insert(std::make_pair(_T("ROOM_ACOUSTICS_VIRTUAL_STEREO"), ROOM_ACOUSTICS_VIRTUAL_STEREO)); + m_setChanger.insert(std::make_pair(_T("ROOM_ACOUSTICS_SPACIAL"), ROOM_ACOUSTICS_SPACIAL)); + m_setChanger.insert(std::make_pair(_T("ROOM_ACOUSTICS_ETHEREAL"), ROOM_ACOUSTICS_ETHEREAL)); + m_setChanger.insert(std::make_pair(_T("ROOM_ACOUSTICS_3D_VOICE"), ROOM_ACOUSTICS_3D_VOICE)); + m_setChanger.insert(std::make_pair(_T("VOICE_CHANGER_EFFECT_UNCLE"), VOICE_CHANGER_EFFECT_UNCLE)); + m_setChanger.insert(std::make_pair(_T("VOICE_CHANGER_EFFECT_OLDMAN"), VOICE_CHANGER_EFFECT_OLDMAN)); + m_setChanger.insert(std::make_pair(_T("VOICE_CHANGER_EFFECT_BOY"), VOICE_CHANGER_EFFECT_BOY)); + m_setChanger.insert(std::make_pair(_T("VOICE_CHANGER_EFFECT_SISTER"), VOICE_CHANGER_EFFECT_SISTER)); + m_setChanger.insert(std::make_pair(_T("VOICE_CHANGER_EFFECT_GIRL"), VOICE_CHANGER_EFFECT_GIRL)); + m_setChanger.insert(std::make_pair(_T("VOICE_CHANGER_EFFECT_PIGKING"), VOICE_CHANGER_EFFECT_PIGKING)); + m_setChanger.insert(std::make_pair(_T("VOICE_CHANGER_EFFECT_HULK"), VOICE_CHANGER_EFFECT_HULK)); + m_setChanger.insert(std::make_pair(_T("STYLE_TRANSFORMATION_RNB"), STYLE_TRANSFORMATION_RNB)); + m_setChanger.insert(std::make_pair(_T("STYLE_TRANSFORMATION_POPULAR"), STYLE_TRANSFORMATION_POPULAR)); + m_setChanger.insert(std::make_pair(_T("PITCH_CORRECTION"), PITCH_CORRECTION)); + + + m_setReverbPreSet.insert(std::make_pair(_T("VOICE_BEAUTIFIER_OFF"), VOICE_BEAUTIFIER_OFF)); + m_setReverbPreSet.insert(std::make_pair(_T("CHAT_BEAUTIFIER_MAGNETIC"), CHAT_BEAUTIFIER_MAGNETIC)); + m_setReverbPreSet.insert(std::make_pair(_T("CHAT_BEAUTIFIER_FRESH"), CHAT_BEAUTIFIER_FRESH)); + m_setReverbPreSet.insert(std::make_pair(_T("CHAT_BEAUTIFIER_VITALITY"), CHAT_BEAUTIFIER_VITALITY)); + m_setReverbPreSet.insert(std::make_pair(_T("TIMBRE_TRANSFORMATION_DEEP"), TIMBRE_TRANSFORMATION_DEEP)); + m_setReverbPreSet.insert(std::make_pair(_T("TIMBRE_TRANSFORMATION_MELLOW"), TIMBRE_TRANSFORMATION_MELLOW)); + m_setReverbPreSet.insert(std::make_pair(_T("TIMBRE_TRANSFORMATION_FALSETTO"), TIMBRE_TRANSFORMATION_FALSETTO)); + m_setReverbPreSet.insert(std::make_pair(_T("TIMBRE_TRANSFORMATION_FULL"), TIMBRE_TRANSFORMATION_FULL)); + m_setReverbPreSet.insert(std::make_pair(_T("TIMBRE_TRANSFORMATION_CLEAR"), TIMBRE_TRANSFORMATION_CLEAR)); + m_setReverbPreSet.insert(std::make_pair(_T("TIMBRE_TRANSFORMATION_RESOUNDING"), TIMBRE_TRANSFORMATION_RESOUNDING)); + m_setReverbPreSet.insert(std::make_pair(_T("TIMBRE_TRANSFORMATION_RINGING"), TIMBRE_TRANSFORMATION_RINGING)); + + ResumeStatus(); + return TRUE; +} + + + + +void CAgoraBeautyAudio::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow)//bShwo is true ,show window + { + InitCtrlText(); + RenderLocalVideo(); + } + else { + ResumeStatus(); + } +} + + +void CAgoraBeautyAudio::OnBnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + CString strInfo; + if (!m_joinChannel) { + CString strChannelName; + m_edtChannel.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + std::string szChannelId = cs2utf8(strChannelName); + //join channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + strInfo.Format(_T("join channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + } + } + else { + //leave channel in the engine. + if (0 == m_rtcEngine->leaveChannel()) { + strInfo.Format(_T("leave channel %s"), getCurrentTime()); + } + } + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + +//set audio changer or unset audio changer. +void CAgoraBeautyAudio::OnBnClickedButtonSetAudioChange() +{ + CString strInfo; + if (!m_beautyAudio) + { + CString str; + m_cmbPerverbPreset.GetWindowText(str); + //enable audio beauty. + if (m_setChanger.find(str) != m_setChanger.end()) + { + int param1; + int param2; + m_rtcEngine->setAudioEffectPreset(m_setChanger[str]); + CString strParam; + m_edtParam1.GetWindowText(strParam); + param1 = _ttol(strParam); + m_edtParam2.GetWindowText(strParam); + param2 = _ttol(strParam); + m_rtcEngine->setAudioEffectParameters(m_setChanger[str], param1, param2); + } + if (m_setReverbPreSet.find(str) != m_setReverbPreSet.end()) + { + m_rtcEngine->setVoiceBeautifierPreset(m_setReverbPreSet[str]); + } + strInfo.Format(_T("set :%s")); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_btnSetBeautyAudio.SetWindowText(beautyAudioCtrlUnSetAudioChange); + } + else { + //set audio beauty to VOICE_CHANGER_OFF. + m_rtcEngine->setAudioEffectPreset(AUDIO_EFFECT_OFF); + m_rtcEngine->setVoiceBeautifierPreset(VOICE_BEAUTIFIER_OFF); + m_lstInfo.InsertString(m_lstInfo.GetCount(),_T("unset beauty voice")); + m_btnSetBeautyAudio.SetWindowText(beautyAudioCtrlSetAudioChange); + } + m_beautyAudio = !m_beautyAudio; + +} + + +//EID_JOINCHANNEL_SUCCESS message window handler +LRESULT CAgoraBeautyAudio::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = true; + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + m_btnJoinChannel.EnableWindow(TRUE); + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_localVideoWnd.SetUID(wParam); + //notify parent window + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); + return 0; +} + +//EID_LEAVEHANNEL_SUCCESS message window handler +LRESULT CAgoraBeautyAudio::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + CString strInfo; + strInfo.Format(_T("leave channel success %s"), getCurrentTime()); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return 0; +} + +//EID_USER_JOINED message window handler +LRESULT CAgoraBeautyAudio::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + CString strInfo; + strInfo.Format(_T("%u joined"), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + +//EID_USER_OFFLINE message handler. +LRESULT CAgoraBeautyAudio::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + m_rtcEngine->setupRemoteVideo(canvas); + CString strInfo; + strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + +//EID_REMOTE_VIDEO_STATE_CHANED message window handler. +LRESULT CAgoraBeautyAudio::OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam) +{ + PVideoStateStateChanged stateChanged = (PVideoStateStateChanged)wParam; + if (stateChanged) { + //onRemoteVideoStateChanged + CString strSateInfo; + switch (stateChanged->state) { + case REMOTE_VIDEO_STATE_STARTING: + strSateInfo = _T("REMOTE_VIDEO_STATE_STARTING"); + break; + case REMOTE_VIDEO_STATE_STOPPED: + strSateInfo = _T("strSateInfo"); + break; + case REMOTE_VIDEO_STATE_DECODING: + strSateInfo = _T("REMOTE_VIDEO_STATE_DECODING"); + break; + case REMOTE_VIDEO_STATE_FAILED: + strSateInfo = _T("REMOTE_VIDEO_STATE_FAILED "); + break; + case REMOTE_VIDEO_STATE_FROZEN: + strSateInfo = _T("REMOTE_VIDEO_STATE_FROZEN "); + break; + } + CString strInfo; + strInfo.Format(_T("onRemoteVideoStateChanged: uid=%u, %s"), stateChanged->uid, strSateInfo); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + return 0; +} + + + +/* +note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one +parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). +*/ +void CAudioChangeEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } +} +/* +note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. +parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). +*/ +void CAudioChangeEventHandler::onUserJoined(uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } +} + +/* +note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. +parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. +*/ +void CAudioChangeEventHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } +} +/* +note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. +parameters: + stats: Call statistics. +*/ + +void CAudioChangeEventHandler::onLeaveChannel(const RtcStats& stats) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } +} +/** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. +*/ +void CAudioChangeEventHandler::onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) +{ + if (m_hMsgHanlder) { + PVideoStateStateChanged stateChanged = new VideoStateStateChanged; + stateChanged->uid = uid; + stateChanged->reason = reason; + stateChanged->state = state; + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), (WPARAM)stateChanged, 0); + } +} + + +BOOL CAgoraBeautyAudio::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} + + +void CAgoraBeautyAudio::OnSelchangeListInfoBroadcasting() +{ + int sel = m_lstInfo.GetCurSel(); + if (sel < 0)return; + CString strDetail; + m_lstInfo.GetText(sel, strDetail); + m_staDetail.SetWindowText(strDetail); +} + + +void CAgoraBeautyAudio::OnSelchangeComboAudioChanger() +{ + m_cmbPerverbPreset.ResetContent(); + CString strType; + m_cmbAudioChange.GetWindowText(strType); + int nIndex = 0; + for (auto & str : m_mapBeauty[strType]) + { + m_cmbPerverbPreset.InsertString(nIndex++, str); + } + m_cmbPerverbPreset.SetCurSel(0); +} diff --git a/windows/APIExample/APIExample/Advanced/BeautyAudio/CAgoraBeautyAudio.h b/windows/APIExample/APIExample/Advanced/BeautyAudio/CAgoraBeautyAudio.h new file mode 100644 index 000000000..0b3affdff --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/BeautyAudio/CAgoraBeautyAudio.h @@ -0,0 +1,153 @@ +#pragma once +#include "AGVideoWnd.h" +#include +#include + +class CAudioChangeEventHandler : public IRtcEngineEventHandler +{ +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override; + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override; + /** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. + */ + virtual void onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) override; +private: + HWND m_hMsgHanlder; +}; + + + +class CAgoraBeautyAudio : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraBeautyAudio) + +public: + CAgoraBeautyAudio(CWnd* pParent = nullptr); + virtual ~CAgoraBeautyAudio(); + + enum { IDD = IDD_DIALOG_BEAUTY_AUDIO }; + +public: + //Initialize the Ctrl Text. + void InitCtrlText(); + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //render local video from SDK local capture. + void RenderLocalVideo(); + //resume window status + void ResumeStatus(); + +private: + bool m_joinChannel = false; + bool m_initialize = false; + bool m_beautyAudio = false; + IRtcEngine* m_rtcEngine = nullptr; + CAGVideoWnd m_localVideoWnd; + CAudioChangeEventHandler m_eventHandler; + std::map> m_mapBeauty; + std::mapm_setChanger; + std::mapm_setReverbPreSet; + +protected: + virtual void DoDataExchange(CDataExchange* pDX); + LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam); + DECLARE_MESSAGE_MAP() +public: + CStatic m_staVideoArea; + CListBox m_lstInfo; + CStatic m_staChannel; + CEdit m_edtChannel; + CButton m_btnJoinChannel; + CStatic m_staAudioChange; + CComboBox m_cmbAudioChange; + + + virtual BOOL OnInitDialog(); + virtual BOOL PreTranslateMessage(MSG* pMsg); + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + afx_msg void OnBnClickedButtonJoinchannel(); + afx_msg void OnBnClickedButtonSetAudioChange(); + CStatic m_staDetail; + afx_msg void OnSelchangeListInfoBroadcasting(); + + CComboBox m_cmbPerverbPreset; + CButton m_btnSetBeautyAudio; + CStatic m_staAudioType; + afx_msg void OnSelchangeComboAudioChanger(); + CStatic m_staParam1; + CStatic m_staParam2; + CEdit m_edtParam1; + CEdit m_edtParam2; +}; diff --git a/windows/APIExample/APIExample/Advanced/CrossChannel/CAgoraCrossChannelDlg.cpp b/windows/APIExample/APIExample/Advanced/CrossChannel/CAgoraCrossChannelDlg.cpp new file mode 100644 index 000000000..6458b83b8 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/CrossChannel/CAgoraCrossChannelDlg.cpp @@ -0,0 +1,449 @@ +#include "stdafx.h" +#include "APIExample.h" +#include "CAgoraCrossChannelDlg.h" +#include + +IMPLEMENT_DYNAMIC(CAgoraCrossChannelDlg, CDialogEx) + +CAgoraCrossChannelDlg::CAgoraCrossChannelDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_CROSS_CHANNEL, pParent) +{ + +} + +CAgoraCrossChannelDlg::~CAgoraCrossChannelDlg() +{ +} + +void CAgoraCrossChannelDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannel); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannel); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_STATIC_CROSS_CHANNEL, m_staCrossChannel); + DDX_Control(pDX, IDC_EDIT_CROSS_CHANNEL, m_edtCrossChannel); + DDX_Control(pDX, IDC_STATIC_TOKEN, m_staToken); + DDX_Control(pDX, IDC_EDIT_TOKEN, m_edtToken); + DDX_Control(pDX, IDC_USER_ID, m_staUserID); + DDX_Control(pDX, IDC_EDIT_USER_ID, m_edtUserID); + DDX_Control(pDX, IDC_CROSS_CHANNEL_LIST, m_staCrossChannel); + DDX_Control(pDX, IDC_COMBO_CROSS_CAHNNEL_LIST, m_cmbCrossChannelList); + DDX_Control(pDX, IDC_STATIC_CROSS_CHANNEL, m_staCrossChannel); + DDX_Control(pDX, IDC_CROSS_CHANNEL_LIST, m_staCrossChannelList); + DDX_Control(pDX, IDC_BUTTON_ADD_CROSS_CHANNEL, m_btnAddChannel); + DDX_Control(pDX, IDC_BUTTON_REMOVE_CROSS_CHANNEL2, m_btnRemove); + DDX_Control(pDX, IDC_BUTTON_START_MEDIA_RELAY, m_btnStartMediaRelay); + DDX_Control(pDX, IDC_STATIC_DETAIL, m_staDetails); + DDX_Control(pDX, IDC_BUTTON_UPDATE, m_btnUpdate); +} + + +BEGIN_MESSAGE_MAP(CAgoraCrossChannelDlg, CDialogEx) + ON_WM_SHOWWINDOW() + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraCrossChannelDlg::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraCrossChannelDlg::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraCrossChannelDlg::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraCrossChannelDlg::OnEIDUserOffline) + ON_MESSAGE(WM_MSGID(EID_CHANNEL_MEDIA_RELAY_EVENT), &CAgoraCrossChannelDlg::OnEIDChannelMediaRelayEvent) + ON_MESSAGE(WM_MSGID(EID_CHANNEL_MEDIA_RELAY_STATE_CHNAGENED), &CAgoraCrossChannelDlg::OnEIDChannelMediaRelayStateChanged) + + + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraCrossChannelDlg::OnBnClickedButtonJoinchannel) + ON_BN_CLICKED(IDC_BUTTON_ADD_CROSS_CHANNEL, &CAgoraCrossChannelDlg::OnBnClickedButtonAddCrossChannel) + ON_BN_CLICKED(IDC_BUTTON_REMOVE_CROSS_CHANNEL2, &CAgoraCrossChannelDlg::OnBnClickedButtonRemoveCrossChannel2) + ON_BN_CLICKED(IDC_BUTTON_START_MEDIA_RELAY, &CAgoraCrossChannelDlg::OnBnClickedButtonStartMediaRelay) + ON_LBN_SELCHANGE(IDC_LIST_INFO_BROADCASTING, &CAgoraCrossChannelDlg::OnSelchangeListInfoBroadcasting) + ON_BN_CLICKED(IDC_BUTTON_UPDATE, &CAgoraCrossChannelDlg::OnBnClickedButtonUpdate) +END_MESSAGE_MAP() + + +//Initialize the Ctrl Text. +void CAgoraCrossChannelDlg::InitCtrlText() +{ + m_staChannel.SetWindowText(commonCtrlChannel); + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + m_btnUpdate.SetWindowText(CrossChannelUpdateMediaRelay); + m_btnAddChannel.SetWindowText(CrossChannelAddChannel); + m_btnRemove.SetWindowText(CrossChannelRemoveChannel); + m_btnStartMediaRelay.SetWindowText(CrossChannelStartMediaRelay); + m_staCrossChannel.SetWindowText(CrossChannelCtrlCrossChannel); + m_staCrossChannelList.SetWindowText(CrossChannelCrossChannelList); + m_staToken.SetWindowText(CrossChannelCtrlToken); + m_staUserID.SetWindowText(CrossChannelCtrlUid); +} + + + +//Initialize the Agora SDK +bool CAgoraCrossChannelDlg::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + m_srcInfo = new ChannelMediaInfo; + m_srcInfo->channelName = nullptr; + m_srcInfo->token = nullptr; + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //enable video in the engine. + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + return true; +} + + +//UnInitialize the Agora SDK +void CAgoraCrossChannelDlg::UnInitAgora() +{ + if (m_rtcEngine) { + if (m_joinChannel) + //leave channel + m_joinChannel = !m_rtcEngine->leaveChannel(); + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + if(m_srcInfo->channelName) + delete m_srcInfo->channelName; + delete m_srcInfo; + } +} + +//render local video from SDK local capture. +void CAgoraCrossChannelDlg::RenderLocalVideo() +{ + if (m_rtcEngine) { + //start preview in the engine. + m_rtcEngine->startPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("startPreview")); + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_localVideoWnd.GetSafeHwnd(); + //setup local video in the engine to canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setupLocalVideo")); + } +} + + +//resume window status +void CAgoraCrossChannelDlg::ResumeStatus() +{ + InitCtrlText(); + m_lstInfo.ResetContent(); + m_staDetails.SetWindowText(_T("")); + m_edtChannel.SetWindowText(_T("")); + m_edtCrossChannel.SetWindowText(_T("")); + m_edtToken.SetWindowText(_T("")); + m_edtUserID.SetWindowText(_T("")); + int offset = 0; + for (auto & mediaInfo : m_vecChannelMedias) + { + delete mediaInfo.channelName; + delete mediaInfo.token; + } + m_vecChannelMedias.clear(); + m_joinChannel = false; + m_initialize = false; + m_startMediaRelay = false; +} + + +void CAgoraCrossChannelDlg::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow)//bShwo is true ,show window + { + InitCtrlText(); + RenderLocalVideo(); + } + else { + ResumeStatus(); + } + +} + + +BOOL CAgoraCrossChannelDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + m_localVideoWnd.MoveWindow(&rcArea); + m_localVideoWnd.ShowWindow(SW_SHOW); + ResumeStatus(); + return TRUE; +} + + +BOOL CAgoraCrossChannelDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} + + +void CAgoraCrossChannelDlg::OnBnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + CString strInfo; + if (!m_joinChannel) { + CString strChannelName; + m_edtChannel.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + std::string szChannelId = cs2utf8(strChannelName); + //join channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + strInfo.Format(_T("join channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + //save channel name and token; + m_srcInfo->channelName = new char[szChannelId.size() + 1]; + strcpy_s(const_cast(m_srcInfo->channelName), szChannelId.size() + 1, szChannelId.data()); + m_srcInfo->token = APP_TOKEN; + } + } + else { + //leave channel in the engine. + if (0 == m_rtcEngine->leaveChannel()) { + strInfo.Format(_T("leave channel %s"), getCurrentTime()); + delete m_srcInfo->channelName; + m_srcInfo->channelName = nullptr; + } + } + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + +//add item into combobox +void CAgoraCrossChannelDlg::OnBnClickedButtonAddCrossChannel() +{ + CString strChannel; + CString strUID; + CString strToken; + m_edtCrossChannel.GetWindowText(strChannel); + m_edtToken.GetWindowText(strToken); + m_edtUserID.GetWindowText(strUID); + + if (strChannel.IsEmpty() || strUID.IsEmpty()) + { + AfxMessageBox(_T("The channel and user ID cannot be empty")); + return; + } + ChannelMediaInfo mediaInfo; + std::string szChannel = cs2utf8(strChannel); + std::string szToken = cs2utf8(strToken); + mediaInfo.channelName = new char[strChannel.GetLength() + 1]; + mediaInfo.token = new char[strToken.GetLength() + 1]; + mediaInfo.uid = _ttol(strUID); + strcpy_s(const_cast(mediaInfo.channelName), strChannel.GetLength() + 1, szChannel.data()); + strcpy_s(const_cast(mediaInfo.token), strToken.GetLength() + 1, szToken.data()); + //add mediaInfo to vector. + m_vecChannelMedias.push_back(mediaInfo); + m_cmbCrossChannelList.AddString(strChannel); + m_cmbCrossChannelList.SetCurSel(m_cmbCrossChannelList.GetCount() - 1); +} + +//remove combobox item +void CAgoraCrossChannelDlg::OnBnClickedButtonRemoveCrossChannel2() +{ + int nSel = m_cmbCrossChannelList.GetCurSel(); + if (nSel < 0)return; + CString strChannelName; + m_cmbCrossChannelList.GetWindowText(strChannelName); + std::string szChannelName = cs2utf8(strChannelName); + + int offset = 0; + //erase media info from m_vecChannelMedias + for (auto & mediaInfo : m_vecChannelMedias) + { + if (szChannelName.compare(mediaInfo.channelName) == 0) + { + delete mediaInfo.channelName; + delete mediaInfo.token; + m_vecChannelMedias.erase(m_vecChannelMedias.begin() + offset); + } + offset++; + } + m_cmbCrossChannelList.DeleteString(nSel); + m_cmbCrossChannelList.SetCurSel(m_cmbCrossChannelList.GetCount() - 1); +} + +//start media relay or stop media relay +void CAgoraCrossChannelDlg::OnBnClickedButtonStartMediaRelay() +{ + if (!m_startMediaRelay) + { + int nDestCount = m_vecChannelMedias.size(); + ChannelMediaInfo *lpDestInfos = new ChannelMediaInfo[nDestCount]; + for (int nIndex = 0; nIndex < nDestCount; nIndex++) { + lpDestInfos[nIndex].channelName = m_vecChannelMedias[nIndex].channelName; + lpDestInfos[nIndex].token = m_vecChannelMedias[nIndex].token; + lpDestInfos[nIndex].uid = m_vecChannelMedias[nIndex].uid; + } + ChannelMediaRelayConfiguration cmrc; + cmrc.srcInfo = m_srcInfo; + cmrc.destInfos = lpDestInfos; + cmrc.destCount = nDestCount; + int ret = 0; + //start Channel Media Relay from cmrc. + ret = m_rtcEngine->startChannelMediaRelay(cmrc); + m_lstInfo.AddString(_T("startChannelMediaRelay")); + delete lpDestInfos; + m_btnStartMediaRelay.SetWindowText(CrossChannelStopMediaRelay); + } + else { + //stop Channel Media Relay. + m_rtcEngine->stopChannelMediaRelay(); + m_lstInfo.AddString(_T("stopChannelMediaRelay")); + m_btnStartMediaRelay.SetWindowText(CrossChannelStartMediaRelay); + } + m_startMediaRelay = !m_startMediaRelay; + +} + +//update update Channel Media Relay. +void CAgoraCrossChannelDlg::OnBnClickedButtonUpdate() +{ + if (m_startMediaRelay) + { + int nDestCount = m_vecChannelMedias.size(); + ChannelMediaInfo *lpDestInfos = new ChannelMediaInfo[nDestCount]; + for (int nIndex = 0; nIndex < nDestCount; nIndex++) { + lpDestInfos[nIndex].channelName = m_vecChannelMedias[nIndex].channelName; + lpDestInfos[nIndex].token = m_vecChannelMedias[nIndex].token; + lpDestInfos[nIndex].uid = m_vecChannelMedias[nIndex].uid; + } + ChannelMediaRelayConfiguration cmrc; + cmrc.srcInfo = m_srcInfo; + cmrc.destInfos = lpDestInfos; + cmrc.destCount = nDestCount; + int ret = 0; + //update Channel Media Relay. + ret = m_rtcEngine->updateChannelMediaRelay(cmrc); + m_lstInfo.AddString(_T("updateChannelMediaRelay")); + delete lpDestInfos; + } +} + +void CAgoraCrossChannelDlg::OnSelchangeListInfoBroadcasting() +{ + int sel = m_lstInfo.GetCurSel(); + if (sel < 0)return; + CString strDetail; + m_lstInfo.GetText(sel, strDetail); + m_staDetails.SetWindowText(strDetail); +} + + +LRESULT CAgoraCrossChannelDlg::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = true; + m_btnJoinChannel.EnableWindow(TRUE); + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_srcInfo->uid = wParam; + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_localVideoWnd.SetUID(wParam); + //notify parent window + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); + return TRUE; +} + +LRESULT CAgoraCrossChannelDlg::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + + CString strInfo; + strInfo.Format(_T("leave channel success %s"), getCurrentTime()); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return TRUE; +} + +LRESULT CAgoraCrossChannelDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + CString strInfo; + strInfo.Format(_T("%u joined"), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return TRUE; +} + +LRESULT CAgoraCrossChannelDlg::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + m_rtcEngine->setupRemoteVideo(canvas); + CString strInfo; + strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + + return TRUE; +} + +//media relay state changed handler +LRESULT CAgoraCrossChannelDlg::OnEIDChannelMediaRelayStateChanged(WPARAM wParam, LPARAM lParam) +{ + CHANNEL_MEDIA_RELAY_STATE state = (CHANNEL_MEDIA_RELAY_STATE)wParam; + CHANNEL_MEDIA_RELAY_ERROR code = (CHANNEL_MEDIA_RELAY_ERROR)lParam; + CString strInfo; + strInfo.Format(_T("channel state:%d, code:%d"), state, code); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return TRUE; +} + +// media relay event handler. +LRESULT CAgoraCrossChannelDlg::OnEIDChannelMediaRelayEvent(WPARAM wParam, LPARAM lParam) +{ + CHANNEL_MEDIA_RELAY_EVENT evt = CHANNEL_MEDIA_RELAY_EVENT(wParam); + CString strInfo; + strInfo.Format(_T("channel media event:%d"), evt); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return TRUE; +} + + diff --git a/windows/APIExample/APIExample/Advanced/CrossChannel/CAgoraCrossChannelDlg.h b/windows/APIExample/APIExample/Advanced/CrossChannel/CAgoraCrossChannelDlg.h new file mode 100644 index 000000000..db69eecb1 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/CrossChannel/CAgoraCrossChannelDlg.h @@ -0,0 +1,184 @@ +#pragma once +#include "AGVideoWnd.h" + +class CAgoraCrossChannelEventHandler : public IRtcEngineEventHandler +{ +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) + { + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } + } + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override + { + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } + } + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override + { + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } + } + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override + { + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } + } + + /** Occurs when the state of the media stream relay changes. + * + * The SDK returns the state of the current media relay with any error + * message. + * + * @param state The state code in #CHANNEL_MEDIA_RELAY_STATE. + * @param code The error code in #CHANNEL_MEDIA_RELAY_ERROR. + */ + virtual void onChannelMediaRelayStateChanged(CHANNEL_MEDIA_RELAY_STATE state, CHANNEL_MEDIA_RELAY_ERROR code)override { + if (m_hMsgHanlder) + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_CHANNEL_MEDIA_RELAY_STATE_CHNAGENED), state, code); + } + + /** Reports events during the media stream relay. + * + * @param code The event code in #CHANNEL_MEDIA_RELAY_EVENT. + */ + virtual void onChannelMediaRelayEvent(CHANNEL_MEDIA_RELAY_EVENT code) { + if (m_hMsgHanlder) + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_CHANNEL_MEDIA_RELAY_EVENT), code, 0); + } + +private: + HWND m_hMsgHanlder; +}; + +class CAgoraCrossChannelDlg : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraCrossChannelDlg) + +public: + CAgoraCrossChannelDlg(CWnd* pParent = nullptr); + virtual ~CAgoraCrossChannelDlg(); + + enum { IDD = IDD_DIALOG_CROSS_CHANNEL }; +public: + //Initialize the Ctrl Text. + void InitCtrlText(); + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //render local video from SDK local capture. + void RenderLocalVideo(); + //resume window status + void ResumeStatus(); + + +private: + bool m_joinChannel = false; + bool m_initialize = false; + bool m_startMediaRelay = false; + IRtcEngine* m_rtcEngine = nullptr; + CAGVideoWnd m_localVideoWnd; + CAgoraCrossChannelEventHandler m_eventHandler; + std::vector m_vecChannelMedias; + ChannelMediaInfo * m_srcInfo; + + +protected: + virtual void DoDataExchange(CDataExchange* pDX); + LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + + LRESULT OnEIDChannelMediaRelayStateChanged(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDChannelMediaRelayEvent(WPARAM wParam, LPARAM lParam); + + DECLARE_MESSAGE_MAP() +public: + CStatic m_staVideoArea; + CListBox m_lstInfo; + CStatic m_staChannel; + CEdit m_edtChannel; + CButton m_btnJoinChannel; + CEdit m_edtCrossChannel; + CStatic m_staToken; + CEdit m_edtToken; + CStatic m_staUserID; + CEdit m_edtUserID; + CComboBox m_cmbCrossChannelList; + CStatic m_staCrossChannel; + CStatic m_staCrossChannelList; + CButton m_btnAddChannel; + CButton m_btnRemove; + CButton m_btnStartMediaRelay; + CStatic m_staDetails; + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + virtual BOOL OnInitDialog(); + virtual BOOL PreTranslateMessage(MSG* pMsg); + afx_msg void OnBnClickedButtonJoinchannel(); + afx_msg void OnBnClickedButtonAddCrossChannel(); + afx_msg void OnBnClickedButtonRemoveCrossChannel2(); + afx_msg void OnBnClickedButtonStartMediaRelay(); + afx_msg void OnSelchangeListInfoBroadcasting(); + CButton m_btnUpdate; + afx_msg void OnBnClickedButtonUpdate(); +}; diff --git a/windows/APIExample/APIExample/Advanced/CustomAudioCapture/CAgoraCaptureAudioDlg.cpp b/windows/APIExample/APIExample/Advanced/CustomAudioCapture/CAgoraCaptureAudioDlg.cpp new file mode 100644 index 000000000..3f2e43c69 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/CustomAudioCapture/CAgoraCaptureAudioDlg.cpp @@ -0,0 +1,661 @@ +#include "stdafx.h" +#include "APIExample.h" +#include "CAgoraCaptureAudioDlg.h" +#include "DirectShow/CircleBuffer.hpp" + +IMPLEMENT_DYNAMIC(CAgoraCaptureAduioDlg, CDialogEx) + + +//EID_JOINCHANNEL_SUCCESS message window handler +LRESULT CAgoraCaptureAduioDlg::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = true; + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + m_btnJoinChannel.EnableWindow(TRUE); + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_localVideoWnd.SetUID(wParam); + //notify parent window + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); + + return 0; +} + +//EID_LEAVEHANNEL_SUCCESS message window handler +LRESULT CAgoraCaptureAduioDlg::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + + CString strInfo; + strInfo.Format(_T("leave channel success %s"), getCurrentTime()); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return 0; +} + +//EID_USER_JOINED message window handler +LRESULT CAgoraCaptureAduioDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + CString strInfo; + strInfo.Format(_T("%u joined"), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + +//EID_USER_OFFLINE message handler. +LRESULT CAgoraCaptureAduioDlg::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + m_rtcEngine->setupRemoteVideo(canvas); + CString strInfo; + strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + +//EID_REMOTE_VIDEO_STATE_CHANED message window handler. +LRESULT CAgoraCaptureAduioDlg::OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam) +{ + PVideoStateStateChanged stateChanged = (PVideoStateStateChanged)wParam; + if (stateChanged) { + //onRemoteVideoStateChanged + CString strSateInfo; + switch (stateChanged->state) { + case REMOTE_VIDEO_STATE_STARTING: + strSateInfo = _T("REMOTE_VIDEO_STATE_STARTING"); + break; + case REMOTE_VIDEO_STATE_STOPPED: + strSateInfo = _T("strSateInfo"); + break; + case REMOTE_VIDEO_STATE_DECODING: + strSateInfo = _T("REMOTE_VIDEO_STATE_DECODING"); + break; + case REMOTE_VIDEO_STATE_FAILED: + strSateInfo = _T("REMOTE_VIDEO_STATE_FAILED "); + break; + case REMOTE_VIDEO_STATE_FROZEN: + strSateInfo = _T("REMOTE_VIDEO_STATE_FROZEN "); + break; + } + CString strInfo; + strInfo.Format(_T("onRemoteVideoStateChanged: uid=%u, %s"), stateChanged->uid, strSateInfo); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + return 0; +} + +CAgoraCaptureAduioDlg::CAgoraCaptureAduioDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_CUSTOM_CAPTURE_AUDIO, pParent) +{ + m_audioFrame.buffer = new BYTE[48000 * 4 * 4]; +} + +CAgoraCaptureAduioDlg::~CAgoraCaptureAduioDlg() +{ + if (m_audioFrame.buffer) + { + delete m_audioFrame.buffer; + m_audioFrame.buffer = nullptr; + } +} + +/* + create Agora Rtc Engine and initialize context.set channel property. +*/ +bool CAgoraCaptureAduioDlg::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //enable video in the engine. + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); + //enable audio in the engine. + //m_rtcEngine->enableAudio(); + //m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable audio")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + return true; +} + + +/* + UnInitialize the Agora SDK +*/ +void CAgoraCaptureAduioDlg::UnInitAgora() +{ + if (m_rtcEngine) { + //leave channel. + if (m_joinChannel) + m_joinChannel = !m_rtcEngine->leaveChannel(); + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} + +/* + render local video from SDK local capture. +*/ +void CAgoraCaptureAduioDlg::RenderLocalVideo() +{ + if (m_rtcEngine) { + //start preview in the engine. + m_rtcEngine->startPreview(); + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_localVideoWnd.GetSafeHwnd(); + //setup local video in the engine to canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("render local video")); + } +} + +//set control text from config. +void CAgoraCaptureAduioDlg::InitCtrlText() +{ + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + m_staChannel.SetWindowText(commonCtrlChannel); + m_staAudioDevice.SetWindowText(customAudioCaptureCtrlCaptureAudioDeivce); + m_btnSetAudioCtx.SetWindowText(customAudioCaptureCtrlSetExternlCapture); +} + +void CAgoraCaptureAduioDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_BUTTON_START_CAPUTRE, m_btnSetAudioCtx); + DDX_Control(pDX, IDC_COMBO_CAPTURE_AUDIO_DEVICE, m_cmbAudioDevice); + DDX_Control(pDX, IDC_COMBO_CAPTURE_AUDIO_TYPE, m_cmbAudioType); + DDX_Control(pDX, IDC_STATIC_CAPTUREDEVICE, m_staAudioDevice); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannel); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannel); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); + DDX_Control(pDX, IDC_BUTTON_RENDER_AUDIO, m_btnSetAudioRender); +} + + +BEGIN_MESSAGE_MAP(CAgoraCaptureAduioDlg, CDialogEx) + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraCaptureAduioDlg::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraCaptureAduioDlg::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraCaptureAduioDlg::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraCaptureAduioDlg::OnEIDUserOffline) + ON_MESSAGE(WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), &CAgoraCaptureAduioDlg::OnEIDRemoteVideoStateChanged) + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraCaptureAduioDlg::OnBnClickedButtonJoinchannel) + ON_BN_CLICKED(IDC_BUTTON_START_CAPUTRE, &CAgoraCaptureAduioDlg::OnBnClickedButtonStartCaputre) + ON_WM_SHOWWINDOW() + ON_CBN_SELCHANGE(IDC_COMBO_CAPTURE_AUDIO_DEVICE, &CAgoraCaptureAduioDlg::OnSelchangeComboCaptureAudioDevice) + ON_BN_CLICKED(IDC_BUTTON_RENDER_AUDIO, &CAgoraCaptureAduioDlg::OnBnClickedButtonRenderAudio) +END_MESSAGE_MAP() + + + +//The JoinChannel button's click handler. +//This function either joins or leaves the channel +void CAgoraCaptureAduioDlg::OnBnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + CString strInfo; + if (!m_joinChannel) { + CString strChannelName; + m_edtChannel.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + std::string szChannelId = cs2utf8(strChannelName); + //join channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + strInfo.Format(_T("join channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + } + } + else { + //leave channel in the engine. + if (0 == m_rtcEngine->leaveChannel()) { + strInfo.Format(_T("leave channel %s"), getCurrentTime()); + } + } + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + +// start or stop capture. +// if bEnable is true start capture otherwise stop capture. +void CAgoraCaptureAduioDlg::EnableCaputre(BOOL bEnable) { + WAVEFORMATEX waveFormat; + SIZE_T nBufferSize = 0; + if (bEnable == (BOOL)m_extenalCaptureAudio) + return; + if (bEnable) + { + //select media capture. + m_agAudioCaptureDevice.SelectMediaCap(m_cmbAudioType.GetCurSel()); + //get current audio capture format. + m_agAudioCaptureDevice.GetCurrentAudioCap(&waveFormat); + nBufferSize = waveFormat.nAvgBytesPerSec / AUDIO_CALLBACK_TIMES; + //create capture Buffer. + m_agAudioCaptureDevice.SetCaptureBuffer(nBufferSize, 16, waveFormat.nBlockAlign); + m_audioFrame.avsync_type = 0; + m_audioFrame.bytesPerSample = 2; + m_audioFrame.type = IAudioFrameObserver::FRAME_TYPE_PCM16; + m_audioFrame.channels = waveFormat.nChannels; + m_audioFrame.samplesPerSec = waveFormat.nSamplesPerSec; + m_audioFrame.samples = m_audioFrame.samplesPerSec / 100; + + //set recording audio frame parameters in the engine. + m_rtcEngine->setRecordingAudioFrameParameters(waveFormat.nSamplesPerSec, waveFormat.nChannels, RAW_AUDIO_FRAME_OP_MODE_READ_WRITE, waveFormat.nSamplesPerSec * waveFormat.nChannels / 100); + //create audio capture filter. + if (!m_agAudioCaptureDevice.CreateCaptureFilter()) + return; + //start audio capture. + m_agAudioCaptureDevice.Start(); + } + else { + //stop audio capture. + m_agAudioCaptureDevice.Stop(); + } + m_extenalCaptureAudio = !m_extenalCaptureAudio; +} + +void CAgoraCaptureAduioDlg::PushAudioFrameThread(CAgoraCaptureAduioDlg * self) +{ + agora::util::AutoPtr mediaEngine; + //query interface agora::AGORA_IID_MEDIA_ENGINE in the engine. + mediaEngine.queryInterface(self->m_rtcEngine, agora::AGORA_IID_MEDIA_ENGINE); + int fps = self->m_audioFrame.samplesPerSec / self->m_audioFrame.samples; + while (self->m_extenalCaptureAudio) + { + SIZE_T nSize = self->m_audioFrame.samples * self->m_audioFrame.channels * self->m_audioFrame.bytesPerSample; + unsigned int readByte = 0; + int timestamp = 0; + if (!CircleBuffer::GetInstance()->readBuffer(self->m_audioFrame.buffer, nSize, &readByte, timestamp)) + { + Sleep(1); + continue; + } + CString strInfo; + strInfo.Format(_T("audio Frame buffer size:%d, readByte:%d, timestamp:%d \n"), nSize, readByte, timestamp); + OutputDebugString(strInfo); + self->m_audioFrame.renderTimeMs = 1000 / fps; + mediaEngine->pushAudioFrame(&self->m_audioFrame); + Sleep(1000 / fps); + } +} + +void CAgoraCaptureAduioDlg::PullAudioFrameThread(CAgoraCaptureAduioDlg * self) +{ + int nRet = 0; + agora::util::AutoPtr mediaEngine; + //query interface agora::AGORA_IID_MEDIA_ENGINE in the engine. + mediaEngine.queryInterface(self->m_rtcEngine, agora::AGORA_IID_MEDIA_ENGINE); + IAudioFrameObserver::AudioFrame audioFrame; + audioFrame.avsync_type = 0;//reserved + audioFrame.bytesPerSample = 2; + audioFrame.type = agora::media::IAudioFrameObserver::FRAME_TYPE_PCM16; + audioFrame.channels = self->m_renderAudioInfo.channels; + audioFrame.samples = self->m_renderAudioInfo.sampleRate / 100 * self->m_renderAudioInfo.channels; + audioFrame.samplesPerSec = self->m_renderAudioInfo.sampleRate; + audioFrame.buffer = new BYTE[audioFrame.samples * audioFrame.bytesPerSample]; + while (self->m_extenalRenderAudio ) + { + nRet = mediaEngine->pullAudioFrame(&audioFrame); + if (nRet != 0) + { + Sleep(10); + continue; + } + SIZE_T nSize = audioFrame.samples * audioFrame.bytesPerSample; + self->m_audioRender.Render((BYTE*)audioFrame.buffer, nSize); + } + delete audioFrame.buffer; +} + + + +/* + Start or stop collecting audio devices and + use external audio source. +*/ +void CAgoraCaptureAduioDlg::OnBnClickedButtonStartCaputre() +{ + if ( !m_extenalCaptureAudio ){ + m_btnSetAudioCtx.SetWindowText(customAudioCaptureCtrlCancelExternlCapture); + //use external audio source. + EnableExtendAudioCapture(TRUE); + //start capture + EnableCaputre(TRUE); + CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)PushAudioFrameThread, this, 0, NULL); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("use external audio source")); + } + else { + m_btnSetAudioCtx.SetWindowText(customAudioCaptureCtrlSetExternlCapture); + //use inner audio source. + EnableExtendAudioCapture(FALSE); + //stop capture. + EnableCaputre(FALSE); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("use inner audio source")); + } + +} + + + + +/* + use external audio source. + sdk will not capture. +*/ +BOOL CAgoraCaptureAduioDlg::EnableExtendAudioCapture(BOOL bEnable) +{ + int nRet = 0; + if ( bEnable ) + nRet = m_rtcEngine->setExternalAudioSource(true, m_capAudioInfo.sampleRate, m_capAudioInfo.channels); + else + nRet = m_rtcEngine->setExternalAudioSource(false, m_capAudioInfo.sampleRate, m_capAudioInfo.channels); + return nRet == 0 ? TRUE : FALSE; +} + +//enable external audio sink +BOOL CAgoraCaptureAduioDlg::EnableExternalRenderAudio(BOOL bEnable) +{ + int nRet = 0; + if ( bEnable ) + { + //set external audio sink + nRet = m_rtcEngine->setExternalAudioSink(true, m_renderAudioInfo.sampleRate, m_renderAudioInfo.channels); + m_audioRender.Init(GetSafeHwnd(), m_renderAudioInfo.sampleRate, m_renderAudioInfo.channels, m_renderAudioInfo.sampleByte * 8); + CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)PullAudioFrameThread, this, 0, NULL); + } + else { + //cancel external audio sink + //sample rate and channels will not be used.so you can set any value. + nRet = m_rtcEngine->setExternalAudioSink(false, 0, 0); + } + return nRet == 0 ? TRUE : FALSE; +} + + +//set external audio render click handler. +void CAgoraCaptureAduioDlg::OnBnClickedButtonRenderAudio() +{ + m_extenalRenderAudio = !m_extenalRenderAudio; + if (m_extenalRenderAudio) + { + //set external render audio mode. + EnableExternalRenderAudio(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(),_T("use external audio sink.")); + m_btnSetAudioRender.SetWindowText(customAudioCaptureCtrlCancelAudioRender); + } + else { + //cancel external render audio mode. + EnableExternalRenderAudio(false); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disable external audio sink.")); + m_btnSetAudioRender.SetWindowText(customAudioCaptureCtrlSetAudioRender); + } +} + + + +/* +note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one +parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). +*/ +void CAgoraCaptureAduioDlgEngineEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } +} +/* +note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. +parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). +*/ +void CAgoraCaptureAduioDlgEngineEventHandler::onUserJoined(uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } +} +/* +note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. +parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. +*/ +void CAgoraCaptureAduioDlgEngineEventHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } +} + +/* +note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. +parameters: + stats: Call statistics. +*/ +void CAgoraCaptureAduioDlgEngineEventHandler::onLeaveChannel(const RtcStats& stats) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } +} + +/** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users + (in the Communication profile) or broadcasters (in the Live-broadcast profile) + in the channel exceeds 17. + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. +*/ +void CAgoraCaptureAduioDlgEngineEventHandler::onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) +{ + if (m_hMsgHanlder) { + PVideoStateStateChanged stateChanged = new VideoStateStateChanged; + stateChanged->uid = uid; + stateChanged->reason = reason; + stateChanged->state = state; + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), (WPARAM)stateChanged, 0); + } +} + +/* + initialize dialog, and set control property. +*/ +BOOL CAgoraCaptureAduioDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + m_localVideoWnd.MoveWindow(&rcArea); + m_localVideoWnd.ShowWindow(SW_SHOW); + //create and initialize audio capture object. + m_agAudioCaptureDevice.Create(); + ResumeStatus(); + m_renderAudioInfo.sampleRate = 44100; + m_renderAudioInfo.channels = 2; + m_renderAudioInfo.sampleByte = 2; + return TRUE; +} + + +// update window view and control. +void CAgoraCaptureAduioDlg::UpdateViews() +{ + // render local video + RenderLocalVideo(); + // enumerate device and show. + UpdateDevice(); +} + +// enumerate device and show device in combobox. +void CAgoraCaptureAduioDlg::UpdateDevice() +{ + TCHAR szDevicePath[MAX_PATH]; + SIZE_T nPathLen = MAX_PATH; + AGORA_DEVICE_INFO agDeviceInfo; + nPathLen = MAX_PATH; + m_cmbAudioDevice.ResetContent(); + //enum audio capture device. + if (m_agAudioCaptureDevice.EnumDeviceList()) + { + m_agAudioCaptureDevice.GetCurrentDevice(szDevicePath, &nPathLen); + for (int nIndex = 0; nIndex < m_agAudioCaptureDevice.GetDeviceCount(); nIndex++) { + m_agAudioCaptureDevice.GetDeviceInfo(nIndex, &agDeviceInfo); + m_cmbAudioDevice.InsertString(nIndex, agDeviceInfo.szDeviceName); + } + if (m_cmbAudioDevice.GetCount() > 0) { + m_cmbAudioDevice.SetCurSel(0); + OnSelchangeComboCaptureAudioDevice(); + } + } +} + +// resume window status. +void CAgoraCaptureAduioDlg::ResumeStatus() +{ + m_lstInfo.ResetContent(); + m_btnSetAudioRender.SetWindowText(customAudioCaptureCtrlSetAudioRender); + EnableCaputre(FALSE); + m_edtChannel.SetWindowText(_T("")); + m_joinChannel = false; + m_initialize = false; + m_remoteJoined = false; + m_extenalCaptureAudio = false; + m_extenalRenderAudio = false; +} + +/* + Enumerate all the video capture devices and add to the combo box. +*/ +void CAgoraCaptureAduioDlg::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow) { + //init control text. + InitCtrlText(); + //update window. + UpdateViews(); + } + else { + //resume window status. + ResumeStatus(); + } +} + +//Enumerates the audio capture devices and types, +//and inserts them into the ComboBox +void CAgoraCaptureAduioDlg::OnSelchangeComboCaptureAudioDevice() +{ + TCHAR szDevicePath[MAX_PATH]; + SIZE_T nPathLen = MAX_PATH; + int nSel = m_cmbAudioDevice.GetCurSel(); + WAVEFORMATEX wavFormatEx; + CString strInfo; + + BOOL bSuccess = m_agAudioCaptureDevice.GetCurrentDevice(szDevicePath, &nPathLen); + if (bSuccess) + m_agAudioCaptureDevice.CloseDevice(); + + if (nSel != -1) + if (!m_agAudioCaptureDevice.OpenDevice(nSel))return; + + m_cmbAudioType.ResetContent(); + //enum current device support type. + for (int nIndex = 0; nIndex < m_agAudioCaptureDevice.GetMediaCapCount(); nIndex++) { + m_agAudioCaptureDevice.GetAudioCap(nIndex, &wavFormatEx); + strInfo.Format(_T("%.1fkHz %dbits %dCh"), wavFormatEx.nSamplesPerSec / 1000.0, wavFormatEx.wBitsPerSample, wavFormatEx.nChannels); + m_cmbAudioType.InsertString(nIndex, strInfo); + } + m_cmbAudioType.SetCurSel(0); +} + + +BOOL CAgoraCaptureAduioDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} + + diff --git a/windows/APIExample/APIExample/Advanced/CustomAudioCapture/CAgoraCaptureAudioDlg.h b/windows/APIExample/APIExample/Advanced/CustomAudioCapture/CAgoraCaptureAudioDlg.h new file mode 100644 index 000000000..4cebd18a4 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/CustomAudioCapture/CAgoraCaptureAudioDlg.h @@ -0,0 +1,177 @@ +#pragma once +#include "stdafx.h" +#include "AGVideoWnd.h" +#include "DirectShow/AGDShowAudioCapture.h" +#include +#include "dsound/DSoundRender.h" + + +class CAgoraCaptureAduioDlgEngineEventHandler : public IRtcEngineEventHandler { +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override; + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override; + /** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users + (in the Communication profile) or broadcasters (in the Live-broadcast profile) + in the channel exceeds 17. + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. + */ + virtual void onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed)override; +private: + HWND m_hMsgHanlder; +}; + + +struct AudioInfo +{ + int sampleRate; + int channels; + int sampleByte; +}; + + +class CAgoraCaptureAduioDlg : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraCaptureAduioDlg) + +public: + //agora sdk message handler + LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam); + + CAgoraCaptureAduioDlg(CWnd* pParent = nullptr); + virtual ~CAgoraCaptureAduioDlg(); + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //set control text from config. + void InitCtrlText(); + //render local video from SDK local capture. + void RenderLocalVideo(); + // use external audio source + BOOL EnableExtendAudioCapture(BOOL bEnable); + + //enable external audio sink + BOOL EnableExternalRenderAudio(BOOL bEnable); + + // update window view and control. + void UpdateViews(); + // enumerate device and show device in combobox. + void UpdateDevice(); + // resume window status. + void ResumeStatus(); + // start or stop capture. + // if bEnable is true start capture otherwise stop capture. + void EnableCaputre(BOOL bEnable); + + + + bool m_joinChannel = false; + bool m_initialize = false; + bool m_remoteJoined = false; + bool m_extenalCaptureAudio = false; + bool m_extenalRenderAudio = false; + IRtcEngine* m_rtcEngine = nullptr; + CAGVideoWnd m_localVideoWnd; + CAgoraCaptureAduioDlgEngineEventHandler m_eventHandler; + CAGDShowAudioCapture m_agAudioCaptureDevice; + AudioInfo m_capAudioInfo; + AudioInfo m_renderAudioInfo; + IAudioFrameObserver::AudioFrame m_audioFrame; + DSoundRender m_audioRender; + + enum { IDD = IDD_DIALOG_CUSTOM_CAPTURE_AUDIO }; + +protected: + //push audio frame in work thread. + static void PushAudioFrameThread(CAgoraCaptureAduioDlg* self); + static void PullAudioFrameThread(CAgoraCaptureAduioDlg* self); + virtual void DoDataExchange(CDataExchange* pDX); + afx_msg void OnBnClickedButtonJoinchannel(); + //set external audio capture click handler. + afx_msg void OnBnClickedButtonStartCaputre(); + //set external audio render click handler. + afx_msg void OnBnClickedButtonRenderAudio(); + afx_msg void OnSelchangeComboCaptureAudioDevice(); + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + virtual BOOL OnInitDialog(); + virtual BOOL PreTranslateMessage(MSG* pMsg); + DECLARE_MESSAGE_MAP() +public: + CButton m_btnJoinChannel; + CButton m_btnSetAudioCtx; + CComboBox m_cmbAudioDevice; + CComboBox m_cmbAudioType; + CStatic m_staAudioDevice; + CStatic m_staChannel; + CEdit m_edtChannel; + CStatic m_staVideoArea; + CListBox m_lstInfo; + CButton m_btnSetAudioRender; +}; diff --git a/windows/APIExample/APIExample/Advanced/CustomEncrypt/CAgoraCustomEncryptDlg.cpp b/windows/APIExample/APIExample/Advanced/CustomEncrypt/CAgoraCustomEncryptDlg.cpp new file mode 100644 index 000000000..05dc2c208 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/CustomEncrypt/CAgoraCustomEncryptDlg.cpp @@ -0,0 +1,426 @@ +#include "stdafx.h" +#include "APIExample.h" +#include "CAgoraCustomEncryptDlg.h" + + + +IMPLEMENT_DYNAMIC(CAgoraCustomEncryptDlg, CDialogEx) + +CAgoraCustomEncryptDlg::CAgoraCustomEncryptDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_ORIGINAL_AUDIO, pParent) +{ + +} + +CAgoraCustomEncryptDlg::~CAgoraCustomEncryptDlg() +{ +} + +void CAgoraCustomEncryptDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); + DDX_Control(pDX, IDC_STATIC_DETAIL, m_staDetail); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannel); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannel); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_STATIC_CUSTOM_ENCRYPT, m_staEncrypt); + DDX_Control(pDX, IDC_COMBO_CUSTOM_ENCRYPT, m_cmbEncrypt); + DDX_Control(pDX, IDC_BUTTON_SET_CUSTOM_ENCRYPT, m_btnSetEncrypt); +} + + +BEGIN_MESSAGE_MAP(CAgoraCustomEncryptDlg, CDialogEx) + ON_WM_SHOWWINDOW() + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraCustomEncryptDlg::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraCustomEncryptDlg::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraCustomEncryptDlg::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraCustomEncryptDlg::OnEIDUserOffline) + ON_MESSAGE(WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), &CAgoraCustomEncryptDlg::OnEIDRemoteVideoStateChanged) + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraCustomEncryptDlg::OnBnClickedButtonJoinchannel) + ON_BN_CLICKED(IDC_BUTTON_SET_CUSTOM_ENCRYPT, &CAgoraCustomEncryptDlg::OnBnClickedButtonSetCustomEncrypt) +END_MESSAGE_MAP() + + +//Initialize the Ctrl Text. +void CAgoraCustomEncryptDlg::InitCtrlText() +{ + m_staChannel.SetWindowText(commonCtrlChannel); + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + m_staEncrypt.SetWindowText(customEncryptCtrlEncrypt); + m_btnSetEncrypt.SetWindowText(customEncryptCtrlSetEncrypt); +} + + +//Initialize the Agora SDK +bool CAgoraCustomEncryptDlg::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //enable video in the engine. + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + return true; +} + + +//UnInitialize the Agora SDK +void CAgoraCustomEncryptDlg::UnInitAgora() +{ + if (m_rtcEngine) { + if (m_joinChannel) + //leave channel + m_joinChannel = !m_rtcEngine->leaveChannel(); + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} + +//render local video from SDK local capture. +void CAgoraCustomEncryptDlg::RenderLocalVideo() +{ + if (m_rtcEngine) { + //start preview in the engine. + m_rtcEngine->startPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("startPreview")); + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_localVideoWnd.GetSafeHwnd(); + //setup local video in the engine to canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setupLocalVideo")); + } +} + + +//resume window status +void CAgoraCustomEncryptDlg::ResumeStatus() +{ + InitCtrlText(); + m_staDetail.SetWindowText(_T("")); + m_edtChannel.SetWindowText(_T("")); + m_cmbEncrypt.SetCurSel(0); + m_lstInfo.ResetContent(); + m_joinChannel = false; + m_initialize = false; + m_setEncrypt = false; +} + +BOOL CAgoraCustomEncryptDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + m_localVideoWnd.MoveWindow(&rcArea); + m_localVideoWnd.ShowWindow(SW_SHOW); + int i = 0; + m_cmbEncrypt.InsertString(i++, _T("custom encrypt")); + m_mapPacketObserver.insert(std::make_pair(_T("custom encrypt"), &m_customPacketObserver)); + ResumeStatus(); + return TRUE; +} + + +BOOL CAgoraCustomEncryptDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} + + +void CAgoraCustomEncryptDlg::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow)//bShwo is true ,show window + { + InitCtrlText(); + RenderLocalVideo(); + } + else { + ResumeStatus(); + } + +} + +//clicked JoinChannel +void CAgoraCustomEncryptDlg::OnBnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + CString strInfo; + if (!m_joinChannel) { + CString strChannelName; + m_edtChannel.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + std::string szChannelId = cs2utf8(strChannelName); + //join channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + strInfo.Format(_T("join channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + } + } + else { + //leave channel in the engine. + if (0 == m_rtcEngine->leaveChannel()) { + strInfo.Format(_T("leave channel %s"), getCurrentTime()); + } + } + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + + +//register or unregister +void CAgoraCustomEncryptDlg::OnBnClickedButtonSetCustomEncrypt() +{ + if (!m_setEncrypt) + { + CString strInfo; + CString strEncryptMode; + m_cmbEncrypt.GetWindowText(strEncryptMode); + m_rtcEngine->registerPacketObserver(m_mapPacketObserver[strEncryptMode]); + strInfo.Format(_T("register:%s"), strEncryptMode); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_btnSetEncrypt.SetWindowText(customEncryptCtrlCancelEncrypt); + } + else { + m_rtcEngine->registerPacketObserver(NULL); + m_lstInfo.InsertString(m_lstInfo.GetCount(),_T("unregister success.")); + m_btnSetEncrypt.SetWindowText(customEncryptCtrlSetEncrypt); + } + m_setEncrypt = !m_setEncrypt; +} + + + +//EID_JOINCHANNEL_SUCCESS message window handler. +LRESULT CAgoraCustomEncryptDlg::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = true; + m_btnJoinChannel.EnableWindow(TRUE); + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_localVideoWnd.SetUID(wParam); + //notify parent window + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); + return 0; +} + +//EID_LEAVE_CHANNEL message window handler. +LRESULT CAgoraCustomEncryptDlg::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + + m_joinChannel = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + + CString strInfo; + strInfo.Format(_T("leave channel success %s"), getCurrentTime()); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return 0; +} + +//EID_USER_JOINED message window handler. +LRESULT CAgoraCustomEncryptDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + CString strInfo; + strInfo.Format(_T("%u joined"), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + + +//EID_USER_OFFLINE message window handler. +LRESULT CAgoraCustomEncryptDlg::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + m_rtcEngine->setupRemoteVideo(canvas); + CString strInfo; + strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + + return 0; +} + +//EID_REMOTE_VIDEO_STATE_CHANED message window handler. +LRESULT CAgoraCustomEncryptDlg::OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam) +{ + PVideoStateStateChanged stateChanged = (PVideoStateStateChanged)wParam; + if (stateChanged) { + //onRemoteVideoStateChanged + CString strSateInfo; + switch (stateChanged->state) { + case REMOTE_VIDEO_STATE_STARTING: + strSateInfo = _T("REMOTE_VIDEO_STATE_STARTING"); + break; + case REMOTE_VIDEO_STATE_STOPPED: + strSateInfo = _T("strSateInfo"); + break; + case REMOTE_VIDEO_STATE_DECODING: + strSateInfo = _T("REMOTE_VIDEO_STATE_DECODING"); + break; + case REMOTE_VIDEO_STATE_FAILED: + strSateInfo = _T("REMOTE_VIDEO_STATE_FAILED "); + break; + case REMOTE_VIDEO_STATE_FROZEN: + strSateInfo = _T("REMOTE_VIDEO_STATE_FROZEN "); + break; + } + CString strInfo; + strInfo.Format(_T("onRemoteVideoStateChanged: uid=%u, %s"), stateChanged->uid, strSateInfo); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + return 0; +} + + +/* +note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one +parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). +*/ +void CAgoraCustomEncryptHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } +} +/* +note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. +parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). +*/ +void CAgoraCustomEncryptHandler::onUserJoined(uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } +} + +/* +note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. +parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. +*/ +void CAgoraCustomEncryptHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } +} +/* +note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. +parameters: + stats: Call statistics. +*/ + +void CAgoraCustomEncryptHandler::onLeaveChannel(const RtcStats& stats) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } +} +/** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. +*/ +void CAgoraCustomEncryptHandler::onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) +{ + if (m_hMsgHanlder) { + PVideoStateStateChanged stateChanged = new VideoStateStateChanged; + stateChanged->uid = uid; + stateChanged->reason = reason; + stateChanged->state = state; + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), (WPARAM)stateChanged, 0); + } +} + + diff --git a/windows/APIExample/APIExample/Advanced/CustomEncrypt/CAgoraCustomEncryptDlg.h b/windows/APIExample/APIExample/Advanced/CustomEncrypt/CAgoraCustomEncryptDlg.h new file mode 100644 index 000000000..91031c376 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/CustomEncrypt/CAgoraCustomEncryptDlg.h @@ -0,0 +1,229 @@ +#pragma once +#include "AGVideoWnd.h" + + +class AgoraPacketObserver : public IPacketObserver +{ +public: + AgoraPacketObserver() + { + m_txAudioBuffer.resize(2048); + m_rxAudioBuffer.resize(2048); + m_txVideoBuffer.resize(2048); + m_rxVideoBuffer.resize(2048); + } + virtual bool onSendAudioPacket(Packet& packet) + { + size_t i; + //encrypt the packet + const unsigned char* p = packet.buffer; + const unsigned char* pe = packet.buffer + packet.size; + + + for (i = 0; p < pe && i < m_txAudioBuffer.size(); ++p, ++i) + { + m_txAudioBuffer[i] = *p ^ 0x55; + } + //assign new buffer and the length back to SDK + packet.buffer = &m_txAudioBuffer[0]; + packet.size = i; + return true; + } + + virtual bool onSendVideoPacket(Packet& packet) + { + size_t i; + //encrypt the packet + const unsigned char* p = packet.buffer; + const unsigned char* pe = packet.buffer + packet.size; + for (i = 0; p < pe && i < m_txVideoBuffer.size(); ++p, ++i) + { + m_txVideoBuffer[i] = *p ^ 0x55; + } + //assign new buffer and the length back to SDK + packet.buffer = &m_txVideoBuffer[0]; + packet.size = i; + return true; + } + + virtual bool onReceiveAudioPacket(Packet& packet) + { + size_t i = 0; + //decrypt the packet + const unsigned char* p = packet.buffer; + const unsigned char* pe = packet.buffer + packet.size; + for (i = 0; p < pe && i < m_rxAudioBuffer.size(); ++p, ++i) + { + m_rxAudioBuffer[i] = *p ^ 0x55; + } + //assign new buffer and the length back to SDK + packet.buffer = &m_rxAudioBuffer[0]; + packet.size = i; + return true; + } + + virtual bool onReceiveVideoPacket(Packet& packet) + { + size_t i = 0; + //decrypt the packet + const unsigned char* p = packet.buffer; + const unsigned char* pe = packet.buffer + packet.size; + + + for (i = 0; p < pe && i < m_rxVideoBuffer.size(); ++p, ++i) + { + m_rxVideoBuffer[i] = *p ^ 0x55; + } + //assign new buffer and the length back to SDK + packet.buffer = &m_rxVideoBuffer[0]; + packet.size = i; + return true; + } + +private: + std::vector m_txAudioBuffer; //buffer for sending audio data + std::vector m_txVideoBuffer; //buffer for sending video data + + std::vector m_rxAudioBuffer; //buffer for receiving audio data + std::vector m_rxVideoBuffer; //buffer for receiving video data +}; + + +class CAgoraCustomEncryptHandler : public IRtcEngineEventHandler +{ +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override; + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override; + /** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. + */ + virtual void onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) override; +private: + HWND m_hMsgHanlder; +}; + + + +class CAgoraCustomEncryptDlg : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraCustomEncryptDlg) + +public: + CAgoraCustomEncryptDlg(CWnd* pParent = nullptr); + virtual ~CAgoraCustomEncryptDlg(); + + + enum { IDD = IDD_DIALOG_CUSTOM_ENCRYPT }; +public: + //Initialize the Ctrl Text. + void InitCtrlText(); + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //render local video from SDK local capture. + void RenderLocalVideo(); + //resume window status + void ResumeStatus(); + +private: + bool m_joinChannel = false; + bool m_initialize = false; + bool m_setEncrypt = false; + IRtcEngine* m_rtcEngine = nullptr; + CAGVideoWnd m_localVideoWnd; + CAgoraCustomEncryptHandler m_eventHandler; + AgoraPacketObserver m_customPacketObserver; + std::map m_mapPacketObserver; + + + +protected: + virtual void DoDataExchange(CDataExchange* pDX); + // agora sdk message window handler + LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam); + DECLARE_MESSAGE_MAP() +public: + CStatic m_staVideoArea; + CListBox m_lstInfo; + CStatic m_staDetail; + CStatic m_staChannel; + CEdit m_edtChannel; + CButton m_btnJoinChannel; + CStatic m_staEncrypt; + CComboBox m_cmbEncrypt; + CButton m_btnSetEncrypt; + virtual BOOL OnInitDialog(); + virtual BOOL PreTranslateMessage(MSG* pMsg); + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + afx_msg void OnBnClickedButtonJoinchannel(); + afx_msg void OnBnClickedButtonSetCustomEncrypt(); +}; diff --git a/windows/APIExample/APIExample/Advanced/CustomVideoCapture/CAgoraCaptureVideoDlg.cpp b/windows/APIExample/APIExample/Advanced/CustomVideoCapture/CAgoraCaptureVideoDlg.cpp new file mode 100644 index 000000000..c563f8698 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/CustomVideoCapture/CAgoraCaptureVideoDlg.cpp @@ -0,0 +1,635 @@ +#include "stdafx.h" +#include "APIExample.h" +#include "CAgoraCaptureVideoDlg.h" +#include + +BEGIN_MESSAGE_MAP(CAgoraCaptureVideoDlg, CDialogEx) + ON_WM_SHOWWINDOW() + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraCaptureVideoDlg::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraCaptureVideoDlg::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraCaptureVideoDlg::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraCaptureVideoDlg::OnEIDUserOffline) + ON_MESSAGE(WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), &CAgoraCaptureVideoDlg::OnEIDRemoteVideoStateChanged) + ON_BN_CLICKED(IDC_BUTTON_START_CAPUTRE, &CAgoraCaptureVideoDlg::OnClickedButtonStartCaputre) + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraCaptureVideoDlg::OnClickedButtonJoinchannel) + ON_CBN_SELCHANGE(IDC_COMBO_CAPTURE_VIDEO_DEVICE, &CAgoraCaptureVideoDlg::OnSelchangeComboCaptureVideoDevice) +END_MESSAGE_MAP() + + +//set control text from config. +void CAgoraCaptureVideoDlg::InitCtrlText() +{ + m_staChannelName.SetWindowText(commonCtrlChannel); + m_staCaputreVideo.SetWindowText(customVideoCaptureCtrlCaptureVideoDevice); + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + m_btnSetExtCapture.SetWindowText(customVideoCaptureCtrlSetExternlCapture); +} + +/* + create Agora RTC Engine and initialize context.set channel property. +*/ +bool CAgoraCaptureVideoDlg::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + return true; +} + +/* + stop and release agora rtc engine. +*/ +void CAgoraCaptureVideoDlg::UnInitAgora() +{ + m_cmbVideoDevice.EnableWindow(TRUE); + m_cmbVideoType.EnableWindow(TRUE); + m_btnSetExtCapture.EnableWindow(TRUE); + if (m_rtcEngine) { + if (m_joinChannel) + m_joinChannel = !m_rtcEngine->leaveChannel(); + ResumeStatus(); + EnableExtendVideoCapture(FALSE); + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} + +/** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users + (in the Communication profile) or broadcasters (in the Live-broadcast profile) + in the channel exceeds 17. + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. +*/ +void CAgoraCaptureVideoDlgEngineEventHandler::onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) +{ + if (m_hMsgHanlder) { + PVideoStateStateChanged stateChanged = new VideoStateStateChanged; + stateChanged->uid = uid; + stateChanged->reason = reason; + stateChanged->state = state; + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), (WPARAM)stateChanged, 0); + } +} + +/* + initialize dialog, and set control property. +*/ +BOOL CAgoraCaptureVideoDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + m_localVideoWnd.MoveWindow(&rcArea); + //create and initialize video capture object. + m_agVideoCaptureDevice.Create(); + ResumeStatus(); + return TRUE; +} + +/* + set external video source or cancel. +*/ +BOOL CAgoraCaptureVideoDlg::EnableExtendVideoCapture(BOOL bEnable) +{ + agora::util::AutoPtr mediaEngine; + //query interface agora::AGORA_IID_MEDIA_ENGINE in the engine. + mediaEngine.queryInterface(m_rtcEngine, agora::AGORA_IID_MEDIA_ENGINE); + int nRet = 0; + if (mediaEngine.get() == NULL) + return FALSE; + if (bEnable) { + //set external video source + nRet = mediaEngine->setExternalVideoSource(true, false); + } + else { + //unset external video source + nRet = mediaEngine->setExternalVideoSource(false, false); + } + return nRet == 0 ? TRUE : FALSE; +} + +// update window view and control. +void CAgoraCaptureVideoDlg::UpdateViews() +{ + // render local video + RenderLocalVideo(); + // enumerate device and show. + UpdateDevice(); +} + +// enumerate device and show device in combobox. +void CAgoraCaptureVideoDlg::UpdateDevice() +{ + TCHAR szDevicePath[MAX_PATH] = { 0 }; + SIZE_T nPathLen = MAX_PATH; + CString strInfo; + AGORA_DEVICE_INFO agDeviceInfo; + m_cmbVideoDevice.ResetContent(); + //enum video capture device. + m_agVideoCaptureDevice.EnumDeviceList(); + for (int nIndex = 0; nIndex < m_agVideoCaptureDevice.GetDeviceCount(); nIndex++) { + m_agVideoCaptureDevice.GetDeviceInfo(nIndex, &agDeviceInfo); + m_cmbVideoDevice.InsertString(nIndex, agDeviceInfo.szDeviceName); + } + m_cmbVideoDevice.SetCurSel(0); + OnSelchangeComboCaptureVideoDevice(); +} +// resume window status. +void CAgoraCaptureVideoDlg::ResumeStatus() +{ + m_lstInfo.ResetContent(); + InitCtrlText(); + EnableCaputre(FALSE); + m_joinChannel = false; + m_initialize = false; + m_remoteJoined = false; + m_extenalCaptureVideo = false; + m_edtChannel.SetWindowText(_T("")); +} + +// start or stop capture. +// if bEnable is true start capture otherwise stop capture. +void CAgoraCaptureVideoDlg::EnableCaputre(BOOL bEnable) +{ + if (bEnable == (BOOL)!m_extenalCaptureVideo)return; + + int nIndex = m_cmbVideoType.GetCurSel(); + if (bEnable) + { + //select video capture type. + m_agVideoCaptureDevice.SelectMediaCap(nIndex == -1 ? 0 : nIndex); + VIDEOINFOHEADER videoInfo; + VideoEncoderConfiguration config; + //create video capture filter. + m_agVideoCaptureDevice.CreateCaptureFilter(); + m_agVideoCaptureDevice.GetCurrentVideoCap(&videoInfo); + config.dimensions.width = videoInfo.bmiHeader.biWidth; + config.dimensions.height = videoInfo.bmiHeader.biHeight; + m_videoFrame.stride = videoInfo.bmiHeader.biWidth; + m_videoFrame.height = videoInfo.bmiHeader.biHeight; + m_videoFrame.rotation = 0; + m_videoFrame.cropBottom = 0; + m_videoFrame.cropLeft = 0; + m_videoFrame.cropRight = 0; + m_videoFrame.cropTop = 0; + m_videoFrame.format = agora::media::ExternalVideoFrame::VIDEO_PIXEL_I420; + m_videoFrame.type = agora::media::ExternalVideoFrame::VIDEO_BUFFER_TYPE::VIDEO_BUFFER_RAW_DATA; + m_fps = (int)(10000000ll / videoInfo.AvgTimePerFrame); + //set video encoder configuration. + m_rtcEngine->setVideoEncoderConfiguration(config); + //set render hwnd,image width,image height,identify yuv. + m_d3dRender.Init(m_localVideoWnd.GetSafeHwnd(), + videoInfo.bmiHeader.biWidth, videoInfo.bmiHeader.biHeight, true); + //start video capture. + m_agVideoCaptureDevice.Start(); + } + else { + //video capture stop. + m_agVideoCaptureDevice.Stop(); + //remove video capture filter. + m_agVideoCaptureDevice.RemoveCaptureFilter(); + if (m_rtcEngine) + { + m_rtcEngine->stopPreview(); + m_d3dRender.Close(); + } + } +} + +void CAgoraCaptureVideoDlg::PushVideoFrameThread(CAgoraCaptureVideoDlg * self) +{ + agora::util::AutoPtr mediaEngine; + //query interface agora::AGORA_IID_MEDIA_ENGINE in the engine. + mediaEngine.queryInterface(self->m_rtcEngine, agora::AGORA_IID_MEDIA_ENGINE); + //start preview in the engine. + self -> m_rtcEngine->startPreview(); + while (self->m_extenalCaptureVideo && self->m_joinChannel) + { + if (self->m_videoFrame.format == agora::media::ExternalVideoFrame::VIDEO_PIXEL_I420) { + int bufSize = self->m_videoFrame.stride * self->m_videoFrame.height * 3 / 2; + int timestamp = GetTickCount(); + //read data from custom capture. + if (CAgVideoBuffer::GetInstance()->readBuffer(self->m_buffer, bufSize, timestamp)) { + self->m_videoFrame.timestamp = timestamp; + } + else + { + Sleep(1); + continue; + } + self->m_videoFrame.buffer = self->m_buffer; + //render image buffer to hwnd. + self->m_d3dRender.Render((char*)self->m_buffer); + //push video frame. + mediaEngine->pushVideoFrame(&self->m_videoFrame); + Sleep(1000 / self->m_fps); + } + else { + return; + } + } +} + +/* + set up canvas and local video view. +*/ +void CAgoraCaptureVideoDlg::RenderLocalVideo() +{ + if (m_rtcEngine) { + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_localVideoWnd.GetSafeHwnd(); + //setup local video in the engine to canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("render local video")); + } +} + + +/* + Enumerate all the video capture devices and add to the combo box. +*/ +void CAgoraCaptureVideoDlg::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow) { + //init control text. + InitCtrlText(); + //update window. + UpdateViews(); + } + else { + //resume window status. + ResumeStatus(); + } +} + +/* + start or stop capture,register or unregister video frame observer. +*/ +void CAgoraCaptureVideoDlg::OnClickedButtonStartCaputre() +{ + m_extenalCaptureVideo = !m_extenalCaptureVideo; + if (m_extenalCaptureVideo) + { + if (m_cmbVideoType.GetCurSel() == -1) + { + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("can not set vitrual video capture")); + return; + } + EnableExtendVideoCapture(TRUE); + //register agora video frame observer. + EnableCaputre(TRUE); + + m_btnSetExtCapture.SetWindowText(customVideoCaptureCtrlCancelExternlCapture); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("use extenal video frame observer sucess!")); + + } + else { + EnableCaputre(FALSE); + //unregister agora frame observer. + EnableExtendVideoCapture(FALSE); + m_btnSetExtCapture.SetWindowText(customVideoCaptureCtrlSetExternlCapture); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("restore video frame observer sucess!")); + } +} + +//The JoinChannel button's click handler. +//This function either joins or leaves the channel +void CAgoraCaptureVideoDlg::OnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + CString strInfo; + if (!m_joinChannel) { + CString strChannelName; + m_edtChannel.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + std::string szChannelId = cs2utf8(strChannelName); + //join channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + strInfo.Format(_T("join channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + } + } + else { + //leave channel in the engine. + if (0 == m_rtcEngine->leaveChannel()) { + strInfo.Format(_T("leave channel %s"), getCurrentTime()); + } + } + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + + +//EID_JOINCHANNEL_SUCCESS message window handler. +LRESULT CAgoraCaptureVideoDlg::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_cmbVideoDevice.EnableWindow(FALSE); + m_cmbVideoType.EnableWindow(FALSE); + m_btnSetExtCapture.EnableWindow(FALSE); + m_joinChannel = true; + m_btnJoinChannel.EnableWindow(TRUE); + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_localVideoWnd.SetUID(wParam); + //notify parent window + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); + CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)PushVideoFrameThread, this, 0, NULL); + + return 0; +} + +//EID_LEAVE_CHANNEL message window handler. +LRESULT CAgoraCaptureVideoDlg::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + m_cmbVideoDevice.EnableWindow(TRUE); + m_cmbVideoType.EnableWindow(TRUE); + m_btnSetExtCapture.EnableWindow(TRUE); + m_joinChannel = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + + CString strInfo; + strInfo.Format(_T("leave channel success %s"), getCurrentTime()); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return 0; +} + +//EID_USER_JOINED message window handler. +LRESULT CAgoraCaptureVideoDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + CString strInfo; + strInfo.Format(_T("%u joined"), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + + +//EID_USER_OFFLINE message window handler. +LRESULT CAgoraCaptureVideoDlg::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + m_rtcEngine->setupRemoteVideo(canvas); + CString strInfo; + strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + + return 0; +} + +//EID_REMOTE_VIDEO_STATE_CHANED message window handler. +LRESULT CAgoraCaptureVideoDlg::OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam) +{ + PVideoStateStateChanged stateChanged = (PVideoStateStateChanged)wParam; + if (stateChanged) { + //onRemoteVideoStateChanged + CString strSateInfo; + switch (stateChanged->state) { + case REMOTE_VIDEO_STATE_STARTING: + strSateInfo = _T("REMOTE_VIDEO_STATE_STARTING"); + break; + case REMOTE_VIDEO_STATE_STOPPED: + strSateInfo = _T("strSateInfo"); + break; + case REMOTE_VIDEO_STATE_DECODING: + strSateInfo = _T("REMOTE_VIDEO_STATE_DECODING"); + break; + case REMOTE_VIDEO_STATE_FAILED: + strSateInfo = _T("REMOTE_VIDEO_STATE_FAILED "); + break; + case REMOTE_VIDEO_STATE_FROZEN: + strSateInfo = _T("REMOTE_VIDEO_STATE_FROZEN "); + break; + } + CString strInfo; + strInfo.Format(_T("onRemoteVideoStateChanged: uid=%u, %s"), stateChanged->uid, strSateInfo); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + return 0; +} + +IMPLEMENT_DYNAMIC(CAgoraCaptureVideoDlg, CDialogEx) + +CAgoraCaptureVideoDlg::CAgoraCaptureVideoDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_CUSTOM_CAPTURE_VIDEO, pParent) +{ + m_buffer = new BYTE[1920 * 1280 * 4 * 4]; +} + +CAgoraCaptureVideoDlg::~CAgoraCaptureVideoDlg() +{ + delete m_buffer; +} + +void CAgoraCaptureVideoDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannelName); + DDX_Control(pDX, IDC_STATIC_CAPTUREDEVICE, m_staCaputreVideo); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannel); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_BUTTON_START_CAPUTRE, m_btnSetExtCapture); + DDX_Control(pDX, IDC_COMBO_CAPTURE_VIDEO_DEVICE, m_cmbVideoDevice); + DDX_Control(pDX, IDC_COMBO_CAPTURE_VIDEO_TYPE, m_cmbVideoType); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); +} + +//Enumerates the video capture devices and types, +//and inserts them into the ComboBox +void CAgoraCaptureVideoDlg::OnSelchangeComboCaptureVideoDevice() +{ + TCHAR szDevicePath[MAX_PATH] = { 0 }; + SIZE_T nPathLen = MAX_PATH; + int nSel = m_cmbVideoDevice.GetCurSel(); + + VIDEOINFOHEADER vidInfoHeader; + CString strInfo; + CString strCompress; + //get current device name. + m_cmbVideoType.ResetContent(); + + BOOL bSuccess = m_agVideoCaptureDevice.GetCurrentDevice(szDevicePath, &nPathLen); + if (bSuccess) + m_agVideoCaptureDevice.CloseDevice(); + + if (nSel != -1) { + //open device. + if (!m_agVideoCaptureDevice.OpenDevice(nSel)) + { + return; + } + //create capture filter. + //m_agVideoCaptureDevice.CreateCaptureFilter(); + } + //enumerate video capture device type. + int count = m_agVideoCaptureDevice.GetMediaCapCount(); + for (int nIndex = 0; nIndex < count; nIndex++) { + m_agVideoCaptureDevice.GetVideoCap(nIndex, &vidInfoHeader); + if (vidInfoHeader.bmiHeader.biCompression == 0)continue; + switch (vidInfoHeader.bmiHeader.biCompression) + { + case MAKEFOURCC('I', '4', '2', '0'): + strInfo.Format(_T("%d*%d %dfps(YUV420)"), vidInfoHeader.bmiHeader.biWidth, vidInfoHeader.bmiHeader.biHeight, 10000000ll / vidInfoHeader.AvgTimePerFrame); + break; + case 0x00000000: + strInfo.Format(_T("%d*%d %dfps(RGB24)"), vidInfoHeader.bmiHeader.biWidth, vidInfoHeader.bmiHeader.biHeight, 10000000ll / vidInfoHeader.AvgTimePerFrame); + break; + case MAKEFOURCC('Y', 'U', 'Y', '2'): + strInfo.Format(_T("%d*%d %dfps(YUY2)"), vidInfoHeader.bmiHeader.biWidth, vidInfoHeader.bmiHeader.biHeight, 10000000ll / vidInfoHeader.AvgTimePerFrame); + break; + case MAKEFOURCC('M', 'J', 'P', 'G'): + strInfo.Format(_T("%d*%d %dfps(MJPEG)"), vidInfoHeader.bmiHeader.biWidth, vidInfoHeader.bmiHeader.biHeight, 10000000ll / vidInfoHeader.AvgTimePerFrame); + break; + case MAKEFOURCC('U', 'Y', 'V', 'Y'): + strInfo.Format(_T("%d*%d %dfps(UYVY)"), vidInfoHeader.bmiHeader.biWidth, vidInfoHeader.bmiHeader.biHeight, 10000000ll / vidInfoHeader.AvgTimePerFrame); + break; + } + m_cmbVideoType.InsertString(nIndex, strInfo); + } + m_cmbVideoType.SetCurSel(0); +} + + + + + +/* +note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one +parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). +*/ +void CAgoraCaptureVideoDlgEngineEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } +} + +/* +note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. +parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). +*/ +void CAgoraCaptureVideoDlgEngineEventHandler::onUserJoined(uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } +} +/* +note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. +parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. +*/ +void CAgoraCaptureVideoDlgEngineEventHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } +} +/* +note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. +parameters: + stats: Call statistics. +*/ +void CAgoraCaptureVideoDlgEngineEventHandler::onLeaveChannel(const RtcStats& stats) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } +} + +BOOL CAgoraCaptureVideoDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} diff --git a/windows/APIExample/APIExample/Advanced/CustomVideoCapture/CAgoraCaptureVideoDlg.h b/windows/APIExample/APIExample/Advanced/CustomVideoCapture/CAgoraCaptureVideoDlg.h new file mode 100644 index 000000000..5578a1beb --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/CustomVideoCapture/CAgoraCaptureVideoDlg.h @@ -0,0 +1,159 @@ +#pragma once +#include "AGVideoWnd.h" +#include "DirectShow/AgVideoBuffer.h" +#include "DirectShow/AGDShowVideoCapture.h" +#include "d3d/D3DRender.h" + +class CAgoraCaptureVideoDlgEngineEventHandler : public IRtcEngineEventHandler { +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override; + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override; + /** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users + (in the Communication profile) or broadcasters (in the Live-broadcast profile) + in the channel exceeds 17. + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. + */ + virtual void onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed); + +private: + HWND m_hMsgHanlder; +}; + + +class CAgoraCaptureVideoDlg : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraCaptureVideoDlg) + +public: + // agora sdk message window handler + LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam); + + CAgoraCaptureVideoDlg(CWnd* pParent = nullptr); + virtual ~CAgoraCaptureVideoDlg(); + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //set control text from config. + void InitCtrlText(); + //render local video from SDK local capture. + void RenderLocalVideo(); + //register or unregister agora video frame observer. + BOOL EnableExtendVideoCapture(BOOL bEnable); + + // update window view and control. + void UpdateViews(); + // enumerate device and show device in combobox. + void UpdateDevice(); + // resume window status. + void ResumeStatus(); + // start or stop capture. + // if bEnable is true start capture otherwise stop capture. + void EnableCaputre(BOOL bEnable); + + static void PushVideoFrameThread(CAgoraCaptureVideoDlg *self); + + enum { IDD = IDD_DIALOG_CUSTOM_CAPTURE_VIDEO }; + +protected: + virtual void DoDataExchange(CDataExchange* pDX); + + CAgoraCaptureVideoDlgEngineEventHandler m_eventHandler; + CAGDShowVideoCapture m_agVideoCaptureDevice; + CAGVideoWnd m_localVideoWnd; + agora::media::ExternalVideoFrame m_videoFrame; + int m_fps; + + IRtcEngine* m_rtcEngine = nullptr; + bool m_joinChannel = false; + bool m_initialize = false; + bool m_remoteJoined = false; + bool m_extenalCaptureVideo = false; + BYTE * m_buffer; + D3DRender m_d3dRender; + + DECLARE_MESSAGE_MAP() +public: + CStatic m_staVideoArea; + CStatic m_staChannelName; + CStatic m_staCaputreVideo; + CEdit m_edtChannel; + CButton m_btnJoinChannel; + CButton m_btnSetExtCapture; + CComboBox m_cmbVideoDevice; + CComboBox m_cmbVideoType; + CListBox m_lstInfo; + virtual BOOL OnInitDialog(); + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + afx_msg void OnClickedButtonStartCaputre(); + afx_msg void OnClickedButtonJoinchannel(); + afx_msg void OnSelchangeComboCaptureVideoDevice(); + virtual BOOL PreTranslateMessage(MSG* pMsg); +}; diff --git a/windows/APIExample/APIExample/Advanced/MediaEncrypt/CAgoraMediaEncryptDlg.cpp b/windows/APIExample/APIExample/Advanced/MediaEncrypt/CAgoraMediaEncryptDlg.cpp new file mode 100644 index 000000000..5afc50aeb --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/MediaEncrypt/CAgoraMediaEncryptDlg.cpp @@ -0,0 +1,445 @@ +#include "stdafx.h" +#include "APIExample.h" +#include "CAgoraMediaEncryptDlg.h" + + + +IMPLEMENT_DYNAMIC(CAgoraMediaEncryptDlg, CDialogEx) + +CAgoraMediaEncryptDlg::CAgoraMediaEncryptDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_MEDIA_ENCRYPT, pParent) +{ + +} + +CAgoraMediaEncryptDlg::~CAgoraMediaEncryptDlg() +{ +} + +void CAgoraMediaEncryptDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannel); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannel); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_STATIC_ENCRYPT_MODE, m_staEncryptMode); + DDX_Control(pDX, IDC_COMBO_ENCRYPT_MODE, m_cmbEncryptMode); + DDX_Control(pDX, IDC_STATIC_ENCRYPT_KEY, m_staEncryptKey); + DDX_Control(pDX, IDC_EDIT_ENCRYPT_KEY, m_edtEncryptKey); + DDX_Control(pDX, IDC_BUTTON_SET_MEDIA_ENCRYPT, m_btnSetEncrypt); + DDX_Control(pDX, IDC_STATIC_DETAIL, m_staDetails); +} + + +BEGIN_MESSAGE_MAP(CAgoraMediaEncryptDlg, CDialogEx) + ON_WM_SHOWWINDOW() + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraMediaEncryptDlg::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraMediaEncryptDlg::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraMediaEncryptDlg::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraMediaEncryptDlg::OnEIDUserOffline) + ON_MESSAGE(WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), &CAgoraMediaEncryptDlg::OnEIDRemoteVideoStateChanged) + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraMediaEncryptDlg::OnBnClickedButtonJoinchannel) + ON_BN_CLICKED(IDC_BUTTON_SET_MEDIA_ENCRYPT, &CAgoraMediaEncryptDlg::OnBnClickedButtonSetMediaEncrypt) + ON_LBN_SELCHANGE(IDC_LIST_INFO_BROADCASTING, &CAgoraMediaEncryptDlg::OnSelchangeListInfoBroadcasting) +END_MESSAGE_MAP() + +//Initialize the Ctrl Text. +void CAgoraMediaEncryptDlg::InitCtrlText() +{ + m_staEncryptKey.SetWindowText(mediaEncryptCtrlSecret); + m_staEncryptMode.SetWindowText(mediaEncryptCtrlMode); + m_staChannel.SetWindowText(commonCtrlChannel); + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + m_btnSetEncrypt.SetWindowText(mediaEncryptCtrlSetEncrypt); +} + +//Initialize the Agora SDK +bool CAgoraMediaEncryptDlg::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //enable video in the engine. + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + return true; +} + + +//UnInitialize the Agora SDK +void CAgoraMediaEncryptDlg::UnInitAgora() +{ + if (m_rtcEngine) { + if (m_joinChannel) + //leave channel + m_joinChannel = !m_rtcEngine->leaveChannel(); + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} + +//render local video from SDK local capture. +void CAgoraMediaEncryptDlg::RenderLocalVideo() +{ + if (m_rtcEngine) { + //start preview in the engine. + m_rtcEngine->startPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("startPreview")); + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_localVideoWnd.GetSafeHwnd(); + //setup local video in the engine to canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setupLocalVideo")); + } +} + + +//resume window status +void CAgoraMediaEncryptDlg::ResumeStatus() +{ + InitCtrlText(); + m_cmbEncryptMode.SetCurSel(0); + m_edtChannel.SetWindowText(_T("")); + m_lstInfo.ResetContent(); + m_edtEncryptKey.SetWindowText(_T("")); + m_staDetails.SetWindowText(_T("")); + m_joinChannel = false; + m_initialize = false; + m_setEncrypt = false; +} + + +BOOL CAgoraMediaEncryptDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + m_localVideoWnd.MoveWindow(&rcArea); + m_localVideoWnd.ShowWindow(SW_SHOW); + int nIndex = 0; + m_cmbEncryptMode.InsertString(nIndex++, _T("AES_128_XTS")); + m_cmbEncryptMode.InsertString(nIndex++, _T("AES_128_ECB")); + m_cmbEncryptMode.InsertString(nIndex++, _T("AES_256_XTS")); + m_cmbEncryptMode.InsertString(nIndex++, _T("SM4_128_ECB")); + + m_mapEncryptMode.insert(std::make_pair("AES_128_XTS", AES_128_XTS)); + m_mapEncryptMode.insert(std::make_pair("AES_128_ECB", AES_128_ECB)); + m_mapEncryptMode.insert(std::make_pair("AES_256_XTS", AES_256_XTS)); + m_mapEncryptMode.insert(std::make_pair("SM4_128_ECB", SM4_128_ECB)); + + int i = 0; + ResumeStatus(); + return TRUE; +} + + +BOOL CAgoraMediaEncryptDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} + + +void CAgoraMediaEncryptDlg::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow)//bShwo is true ,show window + { + InitCtrlText(); + RenderLocalVideo(); + } + else { + ResumeStatus(); + } + +} + + +void CAgoraMediaEncryptDlg::OnBnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + CString strInfo; + if (!m_joinChannel) { + CString strChannelName; + m_edtChannel.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + std::string szChannelId = cs2utf8(strChannelName); + //join channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + strInfo.Format(_T("join channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + } + } + else { + //leave channel in the engine. + if (0 == m_rtcEngine->leaveChannel()) { + strInfo.Format(_T("leave channel %s"), getCurrentTime()); + } + } + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + +//set media encrypt button click handler +void CAgoraMediaEncryptDlg::OnBnClickedButtonSetMediaEncrypt() +{ + //get window text to convert utf-8 string + CString strEncryptMode; + m_cmbEncryptMode.GetWindowText(strEncryptMode); + std::string encryption = cs2utf8(strEncryptMode); + CString strSecret; + m_edtEncryptKey.GetWindowText(strSecret); + std::string secret = cs2utf8(strSecret); + EncryptionConfig config; + config.encryptionMode = m_mapEncryptMode[encryption.c_str()]; + config.encryptionKey = secret.c_str(); + //set encrypt mode + m_rtcEngine->enableEncryption(true, config); + CString strInfo; + strInfo.Format(_T("encrypt mode:%s secret:%s"), strEncryptMode, + strSecret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + +// select change for list control handler +void CAgoraMediaEncryptDlg::OnSelchangeListInfoBroadcasting() +{ + int sel = m_lstInfo.GetCurSel(); + if (sel < 0)return; + CString strDetail; + m_lstInfo.GetText(sel, strDetail); + m_staDetails.SetWindowText(strDetail); +} + + +//EID_JOINCHANNEL_SUCCESS message window handler. +LRESULT CAgoraMediaEncryptDlg::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = true; + m_btnJoinChannel.EnableWindow(TRUE); + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_localVideoWnd.SetUID(wParam); + //notify parent window + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); + return 0; +} + +//EID_LEAVE_CHANNEL message window handler. +LRESULT CAgoraMediaEncryptDlg::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + + m_joinChannel = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + + CString strInfo; + strInfo.Format(_T("leave channel success %s"), getCurrentTime()); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return 0; +} + +//EID_USER_JOINED message window handler. +LRESULT CAgoraMediaEncryptDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + CString strInfo; + strInfo.Format(_T("%u joined"), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + + +//EID_USER_OFFLINE message window handler. +LRESULT CAgoraMediaEncryptDlg::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + m_rtcEngine->setupRemoteVideo(canvas); + CString strInfo; + strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + + return 0; +} + +//EID_REMOTE_VIDEO_STATE_CHANED message window handler. +LRESULT CAgoraMediaEncryptDlg::OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam) +{ + PVideoStateStateChanged stateChanged = (PVideoStateStateChanged)wParam; + if (stateChanged) { + //onRemoteVideoStateChanged + CString strSateInfo; + switch (stateChanged->state) { + case REMOTE_VIDEO_STATE_STARTING: + strSateInfo = _T("REMOTE_VIDEO_STATE_STARTING"); + break; + case REMOTE_VIDEO_STATE_STOPPED: + strSateInfo = _T("strSateInfo"); + break; + case REMOTE_VIDEO_STATE_DECODING: + strSateInfo = _T("REMOTE_VIDEO_STATE_DECODING"); + break; + case REMOTE_VIDEO_STATE_FAILED: + strSateInfo = _T("REMOTE_VIDEO_STATE_FAILED "); + break; + case REMOTE_VIDEO_STATE_FROZEN: + strSateInfo = _T("REMOTE_VIDEO_STATE_FROZEN "); + break; + } + CString strInfo; + strInfo.Format(_T("onRemoteVideoStateChanged: uid=%u, %s"), stateChanged->uid, strSateInfo); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + return 0; +} + + +/* +note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one +parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). +*/ +void CAgoraMediaEncryptHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } +} +/* +note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. +parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). +*/ +void CAgoraMediaEncryptHandler::onUserJoined(uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } +} + +/* +note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. +parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. +*/ +void CAgoraMediaEncryptHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } +} +/* +note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. +parameters: + stats: Call statistics. +*/ + +void CAgoraMediaEncryptHandler::onLeaveChannel(const RtcStats& stats) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } +} +/** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. +*/ +void CAgoraMediaEncryptHandler::onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) +{ + if (m_hMsgHanlder) { + PVideoStateStateChanged stateChanged = new VideoStateStateChanged; + stateChanged->uid = uid; + stateChanged->reason = reason; + stateChanged->state = state; + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), (WPARAM)stateChanged, 0); + } +} \ No newline at end of file diff --git a/windows/APIExample/APIExample/Advanced/MediaEncrypt/CAgoraMediaEncryptDlg.h b/windows/APIExample/APIExample/Advanced/MediaEncrypt/CAgoraMediaEncryptDlg.h new file mode 100644 index 000000000..df0b01183 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/MediaEncrypt/CAgoraMediaEncryptDlg.h @@ -0,0 +1,146 @@ +#pragma once +#include "AGVideoWnd.h" +#include + + +class CAgoraMediaEncryptHandler : public IRtcEngineEventHandler +{ +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override; + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override; + /** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. + */ + virtual void onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) override; +private: + HWND m_hMsgHanlder; +}; + + +class CAgoraMediaEncryptDlg : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraMediaEncryptDlg) + +public: + CAgoraMediaEncryptDlg(CWnd* pParent = nullptr); + virtual ~CAgoraMediaEncryptDlg(); + + enum { IDD = IDD_DIALOG_MEDIA_ENCRYPT }; + +protected: + virtual void DoDataExchange(CDataExchange* pDX); + + DECLARE_MESSAGE_MAP() + + +public: + //Initialize the Ctrl Text. + void InitCtrlText(); + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //render local video from SDK local capture. + void RenderLocalVideo(); + //resume window status + void ResumeStatus(); + +private: + bool m_joinChannel = false; + bool m_initialize = false; + bool m_setEncrypt = false; + IRtcEngine* m_rtcEngine = nullptr; + CAGVideoWnd m_localVideoWnd; + CAgoraMediaEncryptHandler m_eventHandler; + // agora sdk message window handler + LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam); +public: + CStatic m_staVideoArea; + CListBox m_lstInfo; + CStatic m_staChannel; + CEdit m_edtChannel; + CButton m_btnJoinChannel; + CStatic m_staEncryptMode; + CComboBox m_cmbEncryptMode; + CStatic m_staEncryptKey; + CEdit m_edtEncryptKey; + CButton m_btnSetEncrypt; + CStatic m_staDetails; + using EncryptMap = std::map ; + EncryptMap m_mapEncryptMode; + + virtual BOOL OnInitDialog(); + virtual BOOL PreTranslateMessage(MSG* pMsg); + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + afx_msg void OnBnClickedButtonJoinchannel(); + afx_msg void OnBnClickedButtonSetMediaEncrypt(); + afx_msg void OnSelchangeListInfoBroadcasting(); +}; diff --git a/windows/APIExample/APIExample/Advanced/MediaIOCustomVideoCaptrue/CAgoraMediaIOVideoCaptureDlg.cpp b/windows/APIExample/APIExample/Advanced/MediaIOCustomVideoCaptrue/CAgoraMediaIOVideoCaptureDlg.cpp new file mode 100644 index 000000000..d1f5efad5 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/MediaIOCustomVideoCaptrue/CAgoraMediaIOVideoCaptureDlg.cpp @@ -0,0 +1,587 @@ +#include "stdafx.h" +#include "APIExample.h" +#include "CAgoraMediaIOVideoCaptureDlg.h" + +BEGIN_MESSAGE_MAP(CAgoraMediaIOVideoCaptureDlg, CDialogEx) + ON_WM_SHOWWINDOW() + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraMediaIOVideoCaptureDlg::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraMediaIOVideoCaptureDlg::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraMediaIOVideoCaptureDlg::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraMediaIOVideoCaptureDlg::OnEIDUserOffline) + ON_MESSAGE(WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), &CAgoraMediaIOVideoCaptureDlg::OnEIDRemoteVideoStateChanged) + ON_BN_CLICKED(IDC_BUTTON_START_CAPUTRE, &CAgoraMediaIOVideoCaptureDlg::OnClickedButtonStartCaputre) + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraMediaIOVideoCaptureDlg::OnClickedButtonJoinchannel) + ON_CBN_SELCHANGE(IDC_COMBO_CAPTURE_VIDEO_DEVICE, &CAgoraMediaIOVideoCaptureDlg::OnSelchangeComboCaptureVideoDevice) +END_MESSAGE_MAP() + + +//set control text from config. +void CAgoraMediaIOVideoCaptureDlg::InitCtrlText() +{ + m_staChannelName.SetWindowText(commonCtrlChannel); + m_staCaputreVideo.SetWindowText(customVideoCaptureCtrlCaptureVideoDevice); + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + m_btnSetExtCapture.SetWindowText(customVideoCaptureCtrlSetExternlCapture); +} + +/* + create Agora RTC Engine and initialize context.set channel property. +*/ +bool CAgoraMediaIOVideoCaptureDlg::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + return true; +} + +/* + stop and release agora rtc engine. +*/ +void CAgoraMediaIOVideoCaptureDlg::UnInitAgora() +{ + m_cmbVideoDevice.EnableWindow(TRUE); + m_cmbVideoType.EnableWindow(TRUE); + m_btnSetExtCapture.EnableWindow(TRUE); + if (m_rtcEngine) { + if (m_joinChannel) + m_joinChannel = !m_rtcEngine->leaveChannel(); + ResumeStatus(); + EnableExtendVideoCapture(FALSE); + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} + +/** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users + (in the Communication profile) or broadcasters (in the Live-broadcast profile) + in the channel exceeds 17. + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. +*/ +void CAgoraMediaIOVideoCaptureDlgEngineEventHandler::onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) +{ + if (m_hMsgHanlder) { + PVideoStateStateChanged stateChanged = new VideoStateStateChanged; + stateChanged->uid = uid; + stateChanged->reason = reason; + stateChanged->state = state; + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), (WPARAM)stateChanged, 0); + } +} + +/* + initialize dialog, and set control property. +*/ +BOOL CAgoraMediaIOVideoCaptureDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + m_localVideoWnd.MoveWindow(&rcArea); + //create and initialize video capture object. + m_agVideoCaptureDevice.Create(); + ResumeStatus(); + return TRUE; +} + +/* + register or unregister agora video Frame Observer. +*/ +BOOL CAgoraMediaIOVideoCaptureDlg::EnableExtendVideoCapture(BOOL bEnable) +{ + agora::util::AutoPtr mediaEngine; + //query interface agora::AGORA_IID_MEDIA_ENGINE in the engine. + mediaEngine.queryInterface(m_rtcEngine, agora::AGORA_IID_MEDIA_ENGINE); + bool bRet = true; + if (mediaEngine.get() == NULL) + return FALSE; + if (bEnable) { + bRet = m_rtcEngine->setVideoSource(&m_videoSouce); + } + return bRet ? TRUE : FALSE; +} + +// update window view and control. +void CAgoraMediaIOVideoCaptureDlg::UpdateViews() +{ + // render local video + RenderLocalVideo(); + // enumerate device and show. + UpdateDevice(); +} + +// enumerate device and show device in combobox. +void CAgoraMediaIOVideoCaptureDlg::UpdateDevice() +{ + TCHAR szDevicePath[MAX_PATH] = { 0 }; + SIZE_T nPathLen = MAX_PATH; + CString strInfo; + AGORA_DEVICE_INFO agDeviceInfo; + m_cmbVideoDevice.ResetContent(); + //enum video capture device. + m_agVideoCaptureDevice.EnumDeviceList(); + for (int nIndex = 0; nIndex < m_agVideoCaptureDevice.GetDeviceCount(); nIndex++) { + m_agVideoCaptureDevice.GetDeviceInfo(nIndex, &agDeviceInfo); + m_cmbVideoDevice.InsertString(nIndex, agDeviceInfo.szDeviceName); + } + m_cmbVideoDevice.SetCurSel(0); + OnSelchangeComboCaptureVideoDevice(); +} +// resume window status. +void CAgoraMediaIOVideoCaptureDlg::ResumeStatus() +{ + m_lstInfo.ResetContent(); + InitCtrlText(); + EnableCaputre(FALSE); + m_joinChannel = false; + m_initialize = false; + m_remoteJoined = false; + m_extenalCaptureVideo = false; + m_edtChannel.SetWindowText(_T("")); + m_videoSouce.Stop(); +} + +// start or stop capture. +// if bEnable is true start capture otherwise stop capture. +void CAgoraMediaIOVideoCaptureDlg::EnableCaputre(BOOL bEnable) +{ + if (bEnable == (BOOL)!m_extenalCaptureVideo)return; + + int nIndex = m_cmbVideoType.GetCurSel(); + if (bEnable) + { + //select video capture type. + m_agVideoCaptureDevice.SelectMediaCap(nIndex == -1 ? 0 : nIndex); + VIDEOINFOHEADER videoInfo; + VideoEncoderConfiguration config; + //create video capture filter. + m_agVideoCaptureDevice.CreateCaptureFilter(); + m_agVideoCaptureDevice.GetCurrentVideoCap(&videoInfo); + config.dimensions.width = videoInfo.bmiHeader.biWidth; + config.dimensions.height = videoInfo.bmiHeader.biHeight; + //set video information + m_videoSouce.SetParameters(false, videoInfo.bmiHeader.biWidth, + videoInfo.bmiHeader.biHeight, 0, (int)(10000000ll / videoInfo.AvgTimePerFrame)); + //set video encoder configuration. + m_rtcEngine->setVideoEncoderConfiguration(config); + //start video capture. + m_agVideoCaptureDevice.Start(); + //start local render. + m_rtcEngine->startPreview(); + } + else { + m_videoSouce.Stop(); + //video capture stop. + m_agVideoCaptureDevice.Stop(); + //remove video capture filter. + m_agVideoCaptureDevice.RemoveCaptureFilter(); + if (m_rtcEngine) + { + m_rtcEngine->stopPreview(); + } + } +} + +/* + set up canvas and local video view. +*/ +void CAgoraMediaIOVideoCaptureDlg::RenderLocalVideo() +{ + if (m_rtcEngine) { + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_localVideoWnd.GetSafeHwnd(); + //setup local video in the engine to canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("render local video")); + } +} + + +/* + Enumerate all the video capture devices and add to the combo box. +*/ +void CAgoraMediaIOVideoCaptureDlg::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow) { + //init control text. + InitCtrlText(); + //update window. + UpdateViews(); + } + else { + //resume window status. + ResumeStatus(); + } +} + +/* + start or stop capture,register or unregister video frame observer. +*/ +void CAgoraMediaIOVideoCaptureDlg::OnClickedButtonStartCaputre() +{ + m_extenalCaptureVideo = !m_extenalCaptureVideo; + if (m_extenalCaptureVideo) + { + if (m_cmbVideoType.GetCurSel() == -1) + { + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("can not set vitrual video capture")); + return; + } + EnableExtendVideoCapture(TRUE); + //register agora video frame observer. + EnableCaputre(TRUE); + m_btnSetExtCapture.SetWindowText(customVideoCaptureCtrlCancelExternlCapture); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("use sucess!")); + + } + else { + EnableCaputre(FALSE); + //unregister agora frame observer. + EnableExtendVideoCapture(FALSE); + m_btnSetExtCapture.SetWindowText(customVideoCaptureCtrlSetExternlCapture); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("restore video frame observer sucess!")); + } +} + +//The JoinChannel button's click handler. +//This function either joins or leaves the channel +void CAgoraMediaIOVideoCaptureDlg::OnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + CString strInfo; + if (!m_joinChannel) { + CString strChannelName; + m_edtChannel.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + std::string szChannelId = cs2utf8(strChannelName); + //join channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + strInfo.Format(_T("join channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + } + } + else { + //leave channel in the engine. + if (0 == m_rtcEngine->leaveChannel()) { + strInfo.Format(_T("leave channel %s"), getCurrentTime()); + } + } + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + + +//EID_JOINCHANNEL_SUCCESS message window handler. +LRESULT CAgoraMediaIOVideoCaptureDlg::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_cmbVideoDevice.EnableWindow(FALSE); + m_cmbVideoType.EnableWindow(FALSE); + m_btnSetExtCapture.EnableWindow(FALSE); + m_joinChannel = true; + m_btnJoinChannel.EnableWindow(TRUE); + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_localVideoWnd.SetUID(wParam); + //notify parent window + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); + //CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)PushVideoFrameTrhead, this, 0, NULL); + + return 0; +} + +//EID_LEAVE_CHANNEL message window handler. +LRESULT CAgoraMediaIOVideoCaptureDlg::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + m_cmbVideoDevice.EnableWindow(TRUE); + m_cmbVideoType.EnableWindow(TRUE); + m_btnSetExtCapture.EnableWindow(TRUE); + m_joinChannel = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + + CString strInfo; + strInfo.Format(_T("leave channel success %s"), getCurrentTime()); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return 0; +} + +//EID_USER_JOINED message window handler. +LRESULT CAgoraMediaIOVideoCaptureDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + CString strInfo; + strInfo.Format(_T("%u joined"), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + + +//EID_USER_OFFLINE message window handler. +LRESULT CAgoraMediaIOVideoCaptureDlg::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + m_rtcEngine->setupRemoteVideo(canvas); + CString strInfo; + strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + + return 0; +} + +//EID_REMOTE_VIDEO_STATE_CHANED message window handler. +LRESULT CAgoraMediaIOVideoCaptureDlg::OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam) +{ + PVideoStateStateChanged stateChanged = (PVideoStateStateChanged)wParam; + if (stateChanged) { + //onRemoteVideoStateChanged + CString strSateInfo; + switch (stateChanged->state) { + case REMOTE_VIDEO_STATE_STARTING: + strSateInfo = _T("REMOTE_VIDEO_STATE_STARTING"); + break; + case REMOTE_VIDEO_STATE_STOPPED: + strSateInfo = _T("strSateInfo"); + break; + case REMOTE_VIDEO_STATE_DECODING: + strSateInfo = _T("REMOTE_VIDEO_STATE_DECODING"); + break; + case REMOTE_VIDEO_STATE_FAILED: + strSateInfo = _T("REMOTE_VIDEO_STATE_FAILED "); + break; + case REMOTE_VIDEO_STATE_FROZEN: + strSateInfo = _T("REMOTE_VIDEO_STATE_FROZEN "); + break; + } + CString strInfo; + strInfo.Format(_T("onRemoteVideoStateChanged: uid=%u, %s"), stateChanged->uid, strSateInfo); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + return 0; +} + +IMPLEMENT_DYNAMIC(CAgoraMediaIOVideoCaptureDlg, CDialogEx) + +CAgoraMediaIOVideoCaptureDlg::CAgoraMediaIOVideoCaptureDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_CUSTOM_CAPTURE_MEDIA_IO_VIDEO, pParent) +{ +} + +CAgoraMediaIOVideoCaptureDlg::~CAgoraMediaIOVideoCaptureDlg() +{ +} + +void CAgoraMediaIOVideoCaptureDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannelName); + DDX_Control(pDX, IDC_STATIC_CAPTUREDEVICE, m_staCaputreVideo); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannel); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_BUTTON_START_CAPUTRE, m_btnSetExtCapture); + DDX_Control(pDX, IDC_COMBO_CAPTURE_VIDEO_DEVICE, m_cmbVideoDevice); + DDX_Control(pDX, IDC_COMBO_CAPTURE_VIDEO_TYPE, m_cmbVideoType); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); +} + +//Enumerates the video capture devices and types, +//and inserts them into the ComboBox +void CAgoraMediaIOVideoCaptureDlg::OnSelchangeComboCaptureVideoDevice() +{ + TCHAR szDevicePath[MAX_PATH] = { 0 }; + SIZE_T nPathLen = MAX_PATH; + int nSel = m_cmbVideoDevice.GetCurSel(); + + VIDEOINFOHEADER vidInfoHeader; + CString strInfo; + CString strCompress; + //get current device name. + m_cmbVideoType.ResetContent(); + + BOOL bSuccess = m_agVideoCaptureDevice.GetCurrentDevice(szDevicePath, &nPathLen); + if (bSuccess) + m_agVideoCaptureDevice.CloseDevice(); + + if (nSel != -1) { + //open device. + if (!m_agVideoCaptureDevice.OpenDevice(nSel)) + { + return; + } + //create capture filter. + //m_agVideoCaptureDevice.CreateCaptureFilter(); + } + //enumerate video capture device type. + int count = m_agVideoCaptureDevice.GetMediaCapCount(); + for (int nIndex = 0; nIndex < count; nIndex++) { + m_agVideoCaptureDevice.GetVideoCap(nIndex, &vidInfoHeader); + if (vidInfoHeader.bmiHeader.biCompression == 0)continue; + switch (vidInfoHeader.bmiHeader.biCompression) + { + case MAKEFOURCC('I', '4', '2', '0'): + + strInfo.Format(_T("%d*%d %dfps(YUV420)"), vidInfoHeader.bmiHeader.biWidth, vidInfoHeader.bmiHeader.biHeight, 10000000ll / vidInfoHeader.AvgTimePerFrame); + break; + case 0x00000000: + + strInfo.Format(_T("%d*%d %dfps(RGB24)"), vidInfoHeader.bmiHeader.biWidth, vidInfoHeader.bmiHeader.biHeight, 10000000ll / vidInfoHeader.AvgTimePerFrame); + break; + case MAKEFOURCC('Y', 'U', 'Y', '2'): + strInfo.Format(_T("%d*%d %dfps(YUY2)"), vidInfoHeader.bmiHeader.biWidth, vidInfoHeader.bmiHeader.biHeight, 10000000ll / vidInfoHeader.AvgTimePerFrame); + break; + case MAKEFOURCC('M', 'J', 'P', 'G'): + strInfo.Format(_T("%d*%d %dfps(MJPEG)"), vidInfoHeader.bmiHeader.biWidth, vidInfoHeader.bmiHeader.biHeight, 10000000ll / vidInfoHeader.AvgTimePerFrame); + break; + case MAKEFOURCC('U', 'Y', 'V', 'Y'): + strInfo.Format(_T("%d*%d %dfps(UYVY)"), vidInfoHeader.bmiHeader.biWidth, vidInfoHeader.bmiHeader.biHeight, 10000000ll / vidInfoHeader.AvgTimePerFrame); + break; + } + m_cmbVideoType.InsertString(nIndex, strInfo); + } + m_cmbVideoType.SetCurSel(0); +} + + + + + +/* +note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one +parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). +*/ +void CAgoraMediaIOVideoCaptureDlgEngineEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } +} + +/* +note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. +parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). +*/ +void CAgoraMediaIOVideoCaptureDlgEngineEventHandler::onUserJoined(uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } +} +/* +note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. +parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. +*/ +void CAgoraMediaIOVideoCaptureDlgEngineEventHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } +} +/* +note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. +parameters: + stats: Call statistics. +*/ +void CAgoraMediaIOVideoCaptureDlgEngineEventHandler::onLeaveChannel(const RtcStats& stats) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } +} + +BOOL CAgoraMediaIOVideoCaptureDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} diff --git a/windows/APIExample/APIExample/Advanced/MediaIOCustomVideoCaptrue/CAgoraMediaIOVideoCaptureDlg.h b/windows/APIExample/APIExample/Advanced/MediaIOCustomVideoCaptrue/CAgoraMediaIOVideoCaptureDlg.h new file mode 100644 index 000000000..62c29379f --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/MediaIOCustomVideoCaptrue/CAgoraMediaIOVideoCaptureDlg.h @@ -0,0 +1,331 @@ +#pragma once +#include "AGVideoWnd.h" +#include "DirectShow/AgVideoBuffer.h" +#include "DirectShow/AGDShowVideoCapture.h" +#include + + +class CAgoraMediaIOVideoCaptureDlgEngineEventHandler : public IRtcEngineEventHandler { +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override; + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override; + /** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users + (in the Communication profile) or broadcasters (in the Live-broadcast profile) + in the channel exceeds 17. + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. + */ + virtual void onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed); + +private: + HWND m_hMsgHanlder; +}; + + +class CAgoraVideoSource :public IVideoSource { + /** Notification for initializing the custom video source. + * + * The SDK triggers this callback to remind you to initialize the custom video source. After receiving this callback, + * you can do some preparation, such as enabling the camera, and then use the return value to tell the SDK whether the + * custom video source is prepared. + * + * @param consumer An IVideoFrameConsumer object that the SDK passes to you. You need to reserve this object and use it + * to send the video frame to the SDK once the custom video source is started. See IVideoFrameConsumer. + * + * @return + * - true: The custom video source is initialized. + * - false: The custom video source is not ready or fails to initialize. The SDK stops and reports the error. + */ + virtual bool onInitialize(IVideoFrameConsumer *consumer) override + { + std::lock_guard m(m_mutex); + m_videoConsumer = consumer; + OutputDebugString(_T("onInitialize\n")); + return true; + } + + /** Notification for disabling the custom video source. + * + * The SDK triggers this callback to remind you to disable the custom video source device. This callback tells you + * that the SDK is about to release the IVideoFrameConsumer object. Ensure that you no longer use IVideoFrameConsumer + * after receiving this callback. + */ + virtual void onDispose() override + { + OutputDebugString(_T("onDispose\n")); + Stop(); + } + + /** Notification for starting the custom video source. + * + * The SDK triggers this callback to remind you to start the custom video source for capturing video. The SDK uses + * IVideoFrameConsumer to receive the video frame that you capture after the video source is started. You must use + * the return value to tell the SDK whether the custom video source is started. + * + * @return + * - true: The custom video source is started. + * - false: The custom video source fails to start. The SDK stops and reports the error. + */ + virtual bool onStart() override + { + OutputDebugString(_T("onStart\n")); + CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadRun, this, 0, NULL); + return true; + } + + + //worker thread to read data and send data to sdk. + static void ThreadRun(CAgoraVideoSource* self) + { + while (!self->m_isExit) + { + //std::lock_guard m(self->mutex); + int bufSize = self->m_width * self->m_height * 3 / 2; + int timestamp = GetTickCount(); + if (!CAgVideoBuffer::GetInstance()->readBuffer(self->m_buffer, bufSize, timestamp)) { + Sleep(1); + continue; + } + if (self->m_videoConsumer) + { + self->m_mutex.lock(); + //consume Raw Video Frame + self->m_videoConsumer->consumeRawVideoFrame(self->m_buffer, ExternalVideoFrame::VIDEO_PIXEL_I420, + self->m_width, self->m_height, self->m_rotation, timestamp); + self->m_mutex.unlock(); + Sleep(1000 / self->m_fps); + } + } + } + + /** Notification for stopping capturing video. + * + * The SDK triggers this callback to remind you to stop capturing video. This callback tells you that the SDK is about + * to stop using IVideoFrameConsumer to receive the video frame that you capture. + */ + virtual void onStop() override + { + OutputDebugString(_T("onStop\n")); + Stop(); + } + + /** Gets the video frame type. + * + * Before you initialize the custom video source, the SDK triggers this callback to query the video frame type. You + * must specify the video frame type in the return value and then pass it to the SDK. + * + * @note Ensure that the video frame type that you specify in this callback is the same as that in the \ref agora::rtc::IVideoFrameConsumer::consumeRawVideoFrame "consumeRawVideoFrame" method. + * + * @return \ref agora::media::ExternalVideoFrame::VIDEO_PIXEL_FORMAT "VIDEO_PIXEL_FORMAT" + */ + virtual agora::media::ExternalVideoFrame::VIDEO_PIXEL_FORMAT getBufferType() override + { + return ExternalVideoFrame::VIDEO_PIXEL_I420; + } + + /** Gets the capture type of the custom video source. + * + * Before you initialize the custom video source, the SDK triggers this callback to query the capture type of the video source. + * You must specify the capture type in the return value and then pass it to the SDK. The SDK enables the corresponding video + * processing algorithm according to the capture type after receiving the video frame. + * + * @return #VIDEO_CAPTURE_TYPE + */ + virtual VIDEO_CAPTURE_TYPE getVideoCaptureType() override + { + return VIDEO_CAPTURE_CAMERA; + } + + + /** Gets the content hint of the custom video source. + * + * If you specify the custom video source as a screen-sharing video, the SDK triggers this callback to query the + * content hint of the video source before you initialize the video source. You must specify the content hint in the + * return value and then pass it to the SDK. The SDK enables the corresponding video processing algorithm according + * to the content hint after receiving the video frame. + * + * @return \ref agora::rtc::VideoContentHint "VideoContentHint" + */ + virtual VideoContentHint getVideoContentHint() override + { + return CONTENT_HINT_DETAILS; + } + + +public: + CAgoraVideoSource() + { + m_buffer = new BYTE[1920 * 1080 * 4 * 4]; + } + + ~CAgoraVideoSource() + { + delete m_buffer; + } + + void Stop() + { + std::lock_guard m(m_mutex); + m_isExit = true; + m_videoConsumer = nullptr; + } + + void SetParameters(bool isExit, int width, int height, int rotation,int fps) + { + std::lock_guard m(m_mutex); + m_isExit = isExit; + m_width = width; + m_height = height; + m_rotation = rotation; + m_fps = fps; + } + +private: + IVideoFrameConsumer * m_videoConsumer; + bool m_isExit; + BYTE * m_buffer; + int m_width; + int m_height; + int m_rotation; + int m_fps; + std::mutex m_mutex; +}; + + + + +class CAgoraMediaIOVideoCaptureDlg : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraMediaIOVideoCaptureDlg) + +public: + // agora sdk message window handler + LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam); + + CAgoraMediaIOVideoCaptureDlg(CWnd* pParent = nullptr); + virtual ~CAgoraMediaIOVideoCaptureDlg(); + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //set control text from config. + void InitCtrlText(); + //render local video from SDK local capture. + void RenderLocalVideo(); + //register or unregister agora video frame observer. + BOOL EnableExtendVideoCapture(BOOL bEnable); + + // update window view and control. + void UpdateViews(); + // enumerate device and show device in combobox. + void UpdateDevice(); + // resume window status. + void ResumeStatus(); + // start or stop capture. + // if bEnable is true start capture otherwise stop capture. + void EnableCaputre(BOOL bEnable); + + + + enum { + IDD = IDD_DIALOG_CUSTOM_CAPTURE_MEDIA_IO_VIDEO + }; + +protected: + virtual void DoDataExchange(CDataExchange* pDX); + + CAgoraMediaIOVideoCaptureDlgEngineEventHandler m_eventHandler; + CAGDShowVideoCapture m_agVideoCaptureDevice; + CAGVideoWnd m_localVideoWnd; + CAgoraVideoSource m_videoSouce; + + IRtcEngine* m_rtcEngine = nullptr; + bool m_joinChannel = false; + bool m_initialize = false; + bool m_remoteJoined = false; + bool m_extenalCaptureVideo = false; + + DECLARE_MESSAGE_MAP() +public: + CStatic m_staVideoArea; + CStatic m_staChannelName; + CStatic m_staCaputreVideo; + CEdit m_edtChannel; + CButton m_btnJoinChannel; + CButton m_btnSetExtCapture; + CComboBox m_cmbVideoDevice; + CComboBox m_cmbVideoType; + CListBox m_lstInfo; + virtual BOOL OnInitDialog(); + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + afx_msg void OnClickedButtonStartCaputre(); + afx_msg void OnClickedButtonJoinchannel(); + afx_msg void OnSelchangeComboCaptureVideoDevice(); + virtual BOOL PreTranslateMessage(MSG* pMsg); +}; diff --git a/windows/APIExample/APIExample/Advanced/MediaPlayer/CAgoraMediaPlayer.cpp b/windows/APIExample/APIExample/Advanced/MediaPlayer/CAgoraMediaPlayer.cpp new file mode 100644 index 000000000..79a66106b --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/MediaPlayer/CAgoraMediaPlayer.cpp @@ -0,0 +1,672 @@ +#include "stdafx.h" +#include "APIExample.h" +#include "CAgoraMediaPlayer.h" + + +IMPLEMENT_DYNAMIC(CAgoraMediaPlayer, CDialogEx) + +CAgoraMediaPlayer::CAgoraMediaPlayer(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_MEDIA_PLAYER, pParent) +{ + +} + +CAgoraMediaPlayer::~CAgoraMediaPlayer() +{ +} + +void CAgoraMediaPlayer::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); + DDX_Control(pDX, IDC_STATIC_DETAIL, m_staDetail); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannel); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannel); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_STATIC_VIDEO_SOURCE, m_staVideoSource); + DDX_Control(pDX, IDC_EDIT_VIDEO_SOURCE, m_edtVideoSource); + DDX_Control(pDX, IDC_BUTTON_OPEN, m_btnOpen); + DDX_Control(pDX, IDC_BUTTON_STOP, m_btnStop); + DDX_Control(pDX, IDC_BUTTON_PLAY, m_btnPlay); + DDX_Control(pDX, IDC_BUTTON_ATTACH, m_btnAttchPlayer); + DDX_Control(pDX, IDC_BUTTON_PUBLISH_VIDEO, m_btnPublishVideo); + DDX_Control(pDX, IDC_BUTTON_PUBLISH_AUDIO, m_btnPublishAudio); + DDX_Control(pDX, IDC_SLIDER_VIDEO, m_sldVideo); +} + + +//Initialize the Ctrl Text. +void CAgoraMediaPlayer::InitCtrlText() +{ + m_staVideoSource.SetWindowText(mediaPlayerCtrlVideoSource); + m_btnPlay.SetWindowText(mediaPlayerCtrlPlay); + m_btnOpen.SetWindowText(mediaPlayerCtrlOpen); + m_btnStop.SetWindowText(mediaPlayerCtrlClose); + m_btnPublishAudio.SetWindowText(mediaPlayerCtrlPublishAudio); + m_btnPublishVideo.SetWindowText(mediaPlayerCtrlPublishVideo); + m_btnAttchPlayer.SetWindowText(mediaPlayerCtrlAttachPlayer); + m_staChannel.SetWindowText(commonCtrlChannel); + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); +} + +//Initialize media player. +void CAgoraMediaPlayer::InitMediaPlayerKit() +{ + //create agora media player. + m_mediaPlayer = createAgoraMediaPlayer(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("createAgoraMediaPlayer")); + agora::rtc::MediaPlayerContext context; + //initialize media player context. + int ret = m_mediaPlayer->initialize(context); + //set message notify receiver window + m_mediaPlayerEnvet.SetMsgReceiver(m_hWnd); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("mediaplayer initialize")); + //set show window handle. + ret = m_mediaPlayer->setView((agora::media::base::view_t)m_localVideoWnd.GetSafeHwnd()); + //register player event observer. + ret = m_mediaPlayer->registerPlayerObserver(&m_mediaPlayerEnvet); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("registerPlayerObserver")); +} + + +//Uninitialized media player . +void CAgoraMediaPlayer::UnInitMediaPlayerKit() +{ + if (m_mediaPlayer) + { + //call media player release function. + m_mediaPlayer->release(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release mediaPlayer")); + m_mediaPlayer = nullptr; + } +} + +//Initialize the Agora SDK +bool CAgoraMediaPlayer::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //enable video in the engine. + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + VideoEncoderConfiguration config; + config.dimensions = VideoDimensions(640, 360); + config.bitrate = 1200; + config.frameRate = FRAME_RATE_FPS_15; + //set video encoder configuration to engine. + m_rtcEngine->setVideoEncoderConfiguration(config); + //init media player kit. + InitMediaPlayerKit(); + return true; +} + + +//UnInitialize the Agora SDK +void CAgoraMediaPlayer::UnInitAgora() +{ + if (m_rtcEngine) { + if (m_joinChannel) + //leave channel + m_joinChannel = !m_rtcEngine->leaveChannel(); + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } + //release media player kit. + UnInitMediaPlayerKit(); +} + + +//resume window status +void CAgoraMediaPlayer::ResumeStatus() +{ + InitCtrlText(); + m_staDetail.SetWindowText(_T("")); + m_edtChannel.SetWindowText(_T("")); + m_edtVideoSource.SetWindowText(_T("")); + m_lstInfo.ResetContent(); + m_sldVideo.SetPos(0); + m_btnPublishAudio.EnableWindow(FALSE); + m_btnPublishVideo.EnableWindow(FALSE); + m_btnAttchPlayer.EnableWindow(FALSE); + m_btnPlay.EnableWindow(FALSE); + m_mediaPlayerState = mediaPLAYER_READY; + m_joinChannel = false; + m_initialize = false; + m_attach = false; +} + +BEGIN_MESSAGE_MAP(CAgoraMediaPlayer, CDialogEx) + ON_WM_SHOWWINDOW() + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraMediaPlayer::OnBnClickedButtonJoinchannel) + ON_BN_CLICKED(IDC_BUTTON_OPEN, &CAgoraMediaPlayer::OnBnClickedButtonOpen) + ON_BN_CLICKED(IDC_BUTTON_STOP, &CAgoraMediaPlayer::OnBnClickedButtonStop) + ON_BN_CLICKED(IDC_BUTTON_PLAY, &CAgoraMediaPlayer::OnBnClickedButtonPlay) + ON_BN_CLICKED(IDC_BUTTON_ATTACH, &CAgoraMediaPlayer::OnBnClickedButtonAttach) + ON_BN_CLICKED(IDC_BUTTON_PUBLISH_VIDEO, &CAgoraMediaPlayer::OnBnClickedButtonPublishVideo) + ON_BN_CLICKED(IDC_BUTTON_PUBLISH_AUDIO, &CAgoraMediaPlayer::OnBnClickedButtonPublishAudio) + ON_LBN_SELCHANGE(IDC_LIST_INFO_BROADCASTING, &CAgoraMediaPlayer::OnSelchangeListInfoBroadcasting) + + ON_MESSAGE(WM_MSGID(mediaPLAYER_STATE_CHANGED), &CAgoraMediaPlayer::OnmediaPlayerStateChanged) + ON_MESSAGE(WM_MSGID(mediaPLAYER_POSTION_CHANGED), &CAgoraMediaPlayer::OnmediaPlayerPositionChanged) + + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraMediaPlayer::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraMediaPlayer::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraMediaPlayer::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraMediaPlayer::OnEIDUserOffline) + + ON_WM_DESTROY() + ON_NOTIFY(NM_RELEASEDCAPTURE, IDC_SLIDER_VIDEO, &CAgoraMediaPlayer::OnReleasedcaptureSliderVideo) +END_MESSAGE_MAP() + + +//WM_SHOWWINDOW message handler. +void CAgoraMediaPlayer::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow)//bShwo is true ,show window + { + InitCtrlText(); + } + else { + ResumeStatus(); + } + +} + +//InitDialog handler. +BOOL CAgoraMediaPlayer::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + m_localVideoWnd.MoveWindow(&rcArea); + m_localVideoWnd.ShowWindow(SW_SHOW); + ResumeStatus(); + return TRUE; +} + +//join channel handler. +void CAgoraMediaPlayer::OnBnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + CString strInfo; + if (!m_joinChannel) { + CString strChannelName; + m_edtChannel.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + std::string szChannelId = cs2utf8(strChannelName); + //join channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + strInfo.Format(_T("join channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + } + } + else { + //leave channel in the engine. + if (0 == m_rtcEngine->leaveChannel()) { + strInfo.Format(_T("leave channel %s"), getCurrentTime()); + } + } + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + +//open button click handler. +void CAgoraMediaPlayer::OnBnClickedButtonOpen() +{ + CString strUrl; + CString strInfo; + m_edtVideoSource.GetWindowText(strUrl); + std::string tmp = cs2utf8(strUrl); + switch (m_mediaPlayerState) + { + case mediaPLAYER_READY: + case mediaPLAYER_STOP: + + if (tmp.empty()) + { + AfxMessageBox(_T("you can fill video source.")); + return; + } + //call media player open function + m_mediaPlayer->open(tmp.c_str(), 0); + break; + default: + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("can not open player.")); + break; + } +} + +//stop button click handler. +void CAgoraMediaPlayer::OnBnClickedButtonStop() +{ + if (m_mediaPlayerState == mediaPLAYER_OPEN || + m_mediaPlayerState == mediaPLAYER_PLAYING || + m_mediaPlayerState == mediaPLAYER_PAUSE) + { + //call media player stop function + m_mediaPlayer->stop(); + m_mediaPlayerState = mediaPLAYER_STOP; + m_btnPlay.SetWindowText(mediaPlayerCtrlPlay); + m_btnPlay.EnableWindow(FALSE); + //set slider current position. + m_sldVideo.SetPos(0); + } + else { + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("can not stop player")); + } +} + +// play button click handler. +void CAgoraMediaPlayer::OnBnClickedButtonPlay() +{ + int ret; + switch (m_mediaPlayerState) + { + case mediaPLAYER_PAUSE: + case mediaPLAYER_OPEN: + //call media player play function + ret = m_mediaPlayer->play(); + if (ret == 0) + { + m_mediaPlayerState = mediaPLAYER_PLAYING; + m_btnPlay.SetWindowText(mediaPlayerCtrlPause); + } + break; + case mediaPLAYER_PLAYING: + //call media player pause function + ret = m_mediaPlayer->pause(); + if (ret == 0) + { + m_mediaPlayerState = mediaPLAYER_PAUSE; + m_btnPlay.SetWindowText(mediaPlayerCtrlPlay); + } + break; + default: + break; + } +} + +// attach button click handler. +void CAgoraMediaPlayer::OnBnClickedButtonAttach() +{ + if (!m_attach) + { + //attach media player to rtc engine. + m_rtcChannelPublishHelper.attachPlayerToRtc(m_rtcEngine, m_mediaPlayer); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("attach media player!")); + //media player register media player event. + m_rtcChannelPublishHelper.registerAgoraRtcChannelPublishHelperObserver(&m_mediaPlayerEnvet); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("registerAgoraRtcChannelPublishHelperObserver")); + m_btnAttchPlayer.SetWindowText(mediaPlayerCtrlDettachPlayer); + if (m_mediaPlayerState == mediaPLAYER_PLAYING) + { + m_btnPublishAudio.EnableWindow(TRUE); + m_btnPublishVideo.EnableWindow(TRUE); + m_btnPublishVideo.SetWindowText(mediaPlayerCtrlPublishVideo); + m_btnPublishAudio.SetWindowText(mediaPlayerCtrlPublishAudio); + } + } + else { + //detach media player from rtc engine. + m_rtcChannelPublishHelper.detachPlayerFromRtc(); + m_mediaPlayer->mute(false); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("detach media player!")); + m_btnAttchPlayer.SetWindowText(mediaPlayerCtrlAttachPlayer); + m_btnPublishAudio.EnableWindow(FALSE); + m_btnPublishVideo.EnableWindow(FALSE); + m_btnPublishVideo.SetWindowText(mediaPlayerCtrlPublishVideo); + m_btnPublishAudio.SetWindowText(mediaPlayerCtrlPublishAudio); + } + m_attach = !m_attach; +} + +//push video button click handler. +void CAgoraMediaPlayer::OnBnClickedButtonPublishVideo() +{ + if (!m_publishVideo) { + //push video to channel. + m_rtcChannelPublishHelper.publishVideo(); + m_btnPublishVideo.SetWindowText(mediaPlayerCtrlUnPublishVideo); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("publishVideo")); + } + else { + //un push video to channel. + m_rtcChannelPublishHelper.unpublishVideo(); + m_btnPublishVideo.SetWindowText(mediaPlayerCtrlPublishVideo); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("unpublishVideo")); + } + m_publishVideo = !m_publishVideo; +} + +// push audio button click handler. +void CAgoraMediaPlayer::OnBnClickedButtonPublishAudio() +{ + if (!m_publishAudio) + { + //push audio to channel. + m_rtcChannelPublishHelper.publishAudio(); + m_btnPublishAudio.SetWindowText(mediaPlayerCtrlUnPublishAudio); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("publishAudio")); + + } + else + { + //un push audio to channel. + m_rtcChannelPublishHelper.unpublishAudio(); + m_btnPublishAudio.SetWindowText(mediaPlayerCtrlPublishAudio); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("unPublishAudio")); + } + m_publishAudio = !m_publishAudio; +} + +//show notify information +void CAgoraMediaPlayer::OnSelchangeListInfoBroadcasting() +{ + int sel = m_lstInfo.GetCurSel(); + if (sel < 0)return; + CString strDetail; + m_lstInfo.GetText(sel, strDetail); + m_staDetail.SetWindowText(strDetail); +} + +// intercept enter key +BOOL CAgoraMediaPlayer::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} + + +//media player state changed handler +LRESULT CAgoraMediaPlayer::OnmediaPlayerStateChanged(WPARAM wParam, LPARAM lParam) +{ + CString strState; + CString strError; + switch ((agora::media::MEDIA_PLAYER_STATE)wParam) + { + case agora::media::PLAYER_STATE_OPEN_COMPLETED: + strState = _T("PLAYER_STATE_OPEN_COMPLETED"); + m_mediaPlayerState = mediaPLAYER_OPEN; + m_btnPlay.EnableWindow(TRUE); + int64_t duration; + m_mediaPlayer->getDuration(duration); + m_sldVideo.SetRangeMax((int)duration); + + break; + case agora::media::PLAYER_STATE_OPENING: + strState = _T("PLAYER_STATE_OPENING"); + break; + case agora::media::PLAYER_STATE_IDLE: + strState = _T("PLAYER_STATE_IDLE"); + break; + case agora::media::PLAYER_STATE_PLAYING: + strState = _T("PLAYER_STATE_PLAYING"); + break; + case agora::media::PLAYER_STATE_PLAYBACK_COMPLETED: + strState = _T("PLAYER_STATE_PLAYBACK_COMPLETED"); + break; + case agora::media::PLAYER_STATE_PAUSED: + strState = _T("PLAYER_STATE_PAUSED"); + break; + case agora::media::PLAYER_STATE_STOPPED: + strState = _T("PLAYER_STATE_PAUSED"); + break; + case agora::media::PLAYER_STATE_FAILED: + strState = _T("PLAYER_STATE_FAILED"); + //call media player stop function + m_mediaPlayer->stop(); + break; + default: + strState = _T("PLAYER_STATE_UNKNOWN"); + break; + } + + switch ((agora::media::MEDIA_PLAYER_ERROR)lParam) + { + case agora::media::PLAYER_ERROR_URL_NOT_FOUND: + strError = _T("PLAYER_ERROR_URL_NOT_FOUND"); + break; + case agora::media::PLAYER_ERROR_NONE: + strError = _T("PLAYER_ERROR_NONE"); + break; + case agora::media::PLAYER_ERROR_CODEC_NOT_SUPPORTED: + strError = _T("PLAYER_ERROR_NONE"); + break; + case agora::media::PLAYER_ERROR_INVALID_ARGUMENTS: + strError = _T("PLAYER_ERROR_INVALID_ARGUMENTS"); + break; + case agora::media::PLAY_ERROR_SRC_BUFFER_UNDERFLOW: + strError = _T("PLAY_ERROR_SRC_BUFFER_UNDERFLOW"); + break; + case agora::media::PLAYER_ERROR_INTERNAL: + strError = _T("PLAYER_ERROR_INTERNAL"); + break; + case agora::media::PLAYER_ERROR_INVALID_STATE: + strError = _T("PLAYER_ERROR_INVALID_STATE"); + break; + case agora::media::PLAYER_ERROR_NO_RESOURCE: + strError = _T("PLAYER_ERROR_NO_RESOURCE"); + break; + case agora::media::PLAYER_ERROR_OBJ_NOT_INITIALIZED: + strError = _T("PLAYER_ERROR_OBJ_NOT_INITIALIZED"); + break; + case agora::media::PLAYER_ERROR_INVALID_CONNECTION_STATE: + strError = _T("PLAYER_ERROR_INVALID_CONNECTION_STATE"); + break; + case agora::media::PLAYER_ERROR_UNKNOWN_STREAM_TYPE: + strError = _T("PLAYER_ERROR_UNKNOWN_STREAM_TYPE"); + break; + case agora::media::PLAYER_ERROR_VIDEO_RENDER_FAILED: + strError = _T("PLAYER_ERROR_VIDEO_RENDER_FAILED"); + break; + } + CString strInfo; + strInfo.Format(_T("sta:%s,\nerr:%s"), strState, strError); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return TRUE; +} + +LRESULT CAgoraMediaPlayer::OnmediaPlayerPositionChanged(WPARAM wParam, LPARAM lParam) +{ + int64_t * p = (int64_t*)wParam; + m_sldVideo.SetPos((int)*p); + delete p; + return TRUE; +} + + + +//EID_JOINCHANNEL_SUCCESS message window handler +LRESULT CAgoraMediaPlayer::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = true; + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + m_btnJoinChannel.EnableWindow(TRUE); + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_localVideoWnd.SetUID(wParam); + m_btnAttchPlayer.EnableWindow(TRUE); + m_btnAttchPlayer.SetWindowText(mediaPlayerCtrlAttachPlayer); + //notify parent window + return 0; +} + +//EID_LEAVEHANNEL_SUCCESS message window handler +LRESULT CAgoraMediaPlayer::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + CString strInfo; + strInfo.Format(_T("leave channel success %s"), getCurrentTime()); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return 0; +} + +//EID_USER_JOINED message window handler +LRESULT CAgoraMediaPlayer::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + CString strInfo; + strInfo.Format(_T("%u joined"), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + +//EID_USER_OFFLINE message handler. +LRESULT CAgoraMediaPlayer::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + m_rtcEngine->setupRemoteVideo(canvas); + CString strInfo; + strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + + + + +/* +note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one +parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). +*/ +void CAgoraMediaPlayerHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } +} +/* +note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. +parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). +*/ +void CAgoraMediaPlayerHandler::onUserJoined(uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } +} + +/* +note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. +parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. +*/ +void CAgoraMediaPlayerHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } +} +/* +note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. +parameters: + stats: Call statistics. +*/ + +void CAgoraMediaPlayerHandler::onLeaveChannel(const RtcStats& stats) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } +} + + + +void CAgoraMediaPlayer::OnDestroy() +{ + CDialogEx::OnDestroy(); + UnInitMediaPlayerKit(); +} + + +//drag events +void CAgoraMediaPlayer::OnReleasedcaptureSliderVideo(NMHDR *pNMHDR, LRESULT *pResult) +{ + LPNMCUSTOMDRAW pNMCD = reinterpret_cast(pNMHDR); + int pos = m_sldVideo.GetPos(); + m_mediaPlayer->seek(pos); + *pResult = 0; +} diff --git a/windows/APIExample/APIExample/Advanced/MediaPlayer/CAgoraMediaPlayer.h b/windows/APIExample/APIExample/Advanced/MediaPlayer/CAgoraMediaPlayer.h new file mode 100644 index 000000000..d29a02899 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/MediaPlayer/CAgoraMediaPlayer.h @@ -0,0 +1,216 @@ +#pragma once +#include "AGVideoWnd.h" +#include + +#define VIDEO_SLIDER_RANGE 10000 + +class AgoraMediaPlayerEvent : public AgoraRtcChannelPublishHelperObserver +{ +public: + // set notify window. + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + + /** + * @brief Triggered when the player state changes + * + * @param state New player state + * @param ec Player error message + */ + virtual void onPlayerStateChanged(agora::media::MEDIA_PLAYER_STATE state, + agora::media::MEDIA_PLAYER_ERROR ec) + { + + ::PostMessage(m_hMsgHanlder, WM_MSGID(mediaPLAYER_STATE_CHANGED), (WPARAM)state, (LPARAM) ec); + } + + /** + * @brief Triggered when the player progress changes, once every 1 second + * + * @param position Current playback progress, in seconds + */ + virtual void onPositionChanged(const int64_t position) + { + ::PostMessage(m_hMsgHanlder, WM_MSGID(mediaPLAYER_POSTION_CHANGED), (WPARAM)new int64_t(position), NULL); + } + /** + * @brief Triggered when the player have some event + * + * @param event + */ + virtual void onPlayerEvent(agora::media::MEDIA_PLAYER_EVENT event) + { + + }; + + /** + * @brief Triggered when metadata is obtained + * + * @param type Metadata type + * @param data data + * @param length data length + */ + virtual void onMetadata(agora::media::MEDIA_PLAYER_METADATA_TYPE type, const uint8_t* data, + uint32_t length) + { + + } + + +private: + HWND m_hMsgHanlder; +}; + +class CAgoraMediaPlayerHandler : public agora::rtc::IRtcEngineEventHandler +{ +public: + virtual ~CAgoraMediaPlayerHandler() {} + + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override; + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override; +private: + HWND m_hMsgHanlder; +}; + +// media player state +enum MEDIAPLAYERSTATE +{ + mediaPLAYER_READY, + mediaPLAYER_OPEN, + mediaPLAYER_PLAYING, + mediaPLAYER_PAUSE, + mediaPLAYER_STOP, +}; + + +class CAgoraMediaPlayer : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraMediaPlayer) + +public: + CAgoraMediaPlayer(CWnd* pParent = nullptr); + virtual ~CAgoraMediaPlayer(); + + enum { IDD = IDD_DIALOG_MEDIA_PLAYER }; + +public: + //Initialize the Ctrl Text. + void InitCtrlText(); + //Initialize media player. + void InitMediaPlayerKit(); + //Uninitialized media player . + void UnInitMediaPlayerKit(); + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //resume window status + void ResumeStatus(); + +private: + bool m_joinChannel = false; + bool m_initialize = false; + bool m_attach = false; + bool m_publishVideo = false; + bool m_publishAudio = false; + + IRtcEngine* m_rtcEngine = nullptr; + CAGVideoWnd m_localVideoWnd; + CAgoraMediaPlayerHandler m_eventHandler; + AgoraMediaPlayerEvent m_mediaPlayerEnvet; + IMediaPlayer *m_mediaPlayer = nullptr; + MEDIAPLAYERSTATE m_mediaPlayerState = mediaPLAYER_READY; + AgoraRtcChannelPublishHelper m_rtcChannelPublishHelper; +protected: + virtual void DoDataExchange(CDataExchange* pDX); + LRESULT OnmediaPlayerStateChanged(WPARAM wParam, LPARAM lParam); + LRESULT OnmediaPlayerPositionChanged(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + DECLARE_MESSAGE_MAP() +public: + CStatic m_staVideoArea; + CListBox m_lstInfo; + CStatic m_staDetail; + CStatic m_staChannel; + CEdit m_edtChannel; + CButton m_btnJoinChannel; + CStatic m_staVideoSource; + CEdit m_edtVideoSource; + CButton m_btnOpen; + CButton m_btnStop; + CButton m_btnPlay; + CButton m_btnAttchPlayer; + CButton m_btnPublishVideo; + CButton m_btnPublishAudio; + CSliderCtrl m_sldVideo; + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + virtual BOOL OnInitDialog(); + virtual BOOL PreTranslateMessage(MSG* pMsg); + afx_msg void OnBnClickedButtonJoinchannel(); + afx_msg void OnBnClickedButtonOpen(); + afx_msg void OnBnClickedButtonStop(); + afx_msg void OnBnClickedButtonPlay(); + afx_msg void OnBnClickedButtonAttach(); + afx_msg void OnBnClickedButtonPublishVideo(); + afx_msg void OnBnClickedButtonPublishAudio(); + afx_msg void OnSelchangeListInfoBroadcasting(); + afx_msg void OnDestroy(); + afx_msg void OnReleasedcaptureSliderVideo(NMHDR *pNMHDR, LRESULT *pResult); +}; diff --git a/windows/APIExample/APIExample/Advanced/MultiChannel/CAgoraMultiChannelDlg.cpp b/windows/APIExample/APIExample/Advanced/MultiChannel/CAgoraMultiChannelDlg.cpp new file mode 100644 index 000000000..dbccdca8d --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/MultiChannel/CAgoraMultiChannelDlg.cpp @@ -0,0 +1,550 @@ +#include "stdafx.h" +#include "APIExample.h" +#include "CAgoraMultiChannelDlg.h" + + +IMPLEMENT_DYNAMIC(CAgoraMultiChannelDlg, CDialogEx) + +CAgoraMultiChannelDlg::CAgoraMultiChannelDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_BEAUTY, pParent) +{ + +} + +CAgoraMultiChannelDlg::~CAgoraMultiChannelDlg() +{ +} + +void CAgoraMultiChannelDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannel); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannel); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_STATIC_CHANNEL_LIST, m_staChannelList); + DDX_Control(pDX, IDC_COMBO_CHANNEL_LIST, m_cmbChannelList); + DDX_Control(pDX, IDC_BUTTON_LEAVE_CHANNEL, m_btnLeaveChannel); + DDX_Control(pDX, IDC_STATIC_DETAIL, m_staDetail); +} + + +BEGIN_MESSAGE_MAP(CAgoraMultiChannelDlg, CDialogEx) + ON_WM_SHOWWINDOW() + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraMultiChannelDlg::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraMultiChannelDlg::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraMultiChannelDlg::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraMultiChannelDlg::OnEIDUserOffline) + ON_MESSAGE(WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), &CAgoraMultiChannelDlg::OnEIDRemoteVideoStateChanged) + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraMultiChannelDlg::OnBnClickedButtonJoinchannel) + ON_BN_CLICKED(IDC_BUTTON_LEAVE_CHANNEL, &CAgoraMultiChannelDlg::OnBnClickedButtonLeaveChannel) + ON_LBN_SELCHANGE(IDC_LIST_INFO_BROADCASTING, &CAgoraMultiChannelDlg::OnSelchangeListInfoBroadcasting) +END_MESSAGE_MAP() + + +//Initialize the Ctrl Text. +void CAgoraMultiChannelDlg::InitCtrlText() +{ + m_staChannelList.SetWindowText(MultiChannelCtrlChannelList); + m_staChannel.SetWindowText(commonCtrlChannel); + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + m_btnLeaveChannel.SetWindowText(commonCtrlLeaveChannel); +} + + + +//Initialize the Agora SDK +bool CAgoraMultiChannelDlg::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //enable video in the engine. + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + return true; +} + + +//UnInitialize the Agora SDK +void CAgoraMultiChannelDlg::UnInitAgora() +{ + if (m_rtcEngine) { + if (m_joinChannel) + //leave channel + m_joinChannel = !m_rtcEngine->leaveChannel(); + for (auto &info : m_channels) + { + info.channel->release(); + delete info.evnetHandler; + } + m_channels.clear(); + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} + +//render local video from SDK local capture. +void CAgoraMultiChannelDlg::RenderLocalVideo() +{ + if (m_rtcEngine) { + //start preview in the engine. + m_rtcEngine->startPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("startPreview")); + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_localVideoWnd.GetSafeHwnd(); + //setup local video in the engine to canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setupLocalVideo")); + } +} + + +//resume window status +void CAgoraMultiChannelDlg::ResumeStatus() +{ + InitCtrlText(); + m_lstInfo.ResetContent(); + m_staDetail.SetWindowText(_T("")); + m_edtChannel.SetWindowText(_T("")); + m_cmbChannelList.ResetContent(); + for (auto &info:m_channels) + { + info.channel->release(); + delete info.evnetHandler; + } + m_channels.clear(); + m_joinChannel = false; + m_initialize = false; + m_audioMixing = false; +} + + + +void CAgoraMultiChannelDlg::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow)//bShwo is true ,show window + { + InitCtrlText(); + RenderLocalVideo(); + } + else { + ResumeStatus(); + } +} + + +BOOL CAgoraMultiChannelDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + m_localVideoWnd.MoveWindow(&rcArea); + m_localVideoWnd.ShowWindow(SW_SHOW); + ResumeStatus(); + return TRUE; +} + + +BOOL CAgoraMultiChannelDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} + + +void CAgoraMultiChannelDlg::OnBnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + CString strInfo; + CString strChannelName; + m_edtChannel.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + std::string szChannelId = cs2utf8(strChannelName); + if (!m_joinChannel) { + //join main channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + m_strMainChannel = strChannelName; + m_cmbChannelList.InsertString(m_cmbChannelList.GetCount(), strChannelName); + m_cmbChannelList.SetCurSel(0); + m_btnJoinChannel.EnableWindow(FALSE); + strInfo.Format(_T("join channel:%s ...."), strChannelName); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + } + else { + CString strTmp; + for (int nIndex = 0; nIndex < m_cmbChannelList.GetCount(); nIndex++) + { + m_cmbChannelList.GetLBText(nIndex, strTmp); + if (strTmp.Trim() == strChannelName) + { + AfxMessageBox(_T("you joined this channel!")); + return; + } + } + //create channel by channel id. + IChannel * pChannel = static_cast(m_rtcEngine)->createChannel(szChannelId.c_str()); + //create channel event handler. + ChannelEventHandler* pEvt = new ChannelEventHandler; + //set message receiver window. + pEvt->setMsgHandler(GetSafeHwnd()); + //add channels. + m_channels.emplace_back(szChannelId, pChannel, pEvt); + //set channel event handler. + pChannel->setChannelEventHandler(pEvt); + ChannelMediaOptions options; + options.autoSubscribeAudio = true; + options.autoSubscribeVideo = true; + pChannel->setClientRole(CLIENT_ROLE_BROADCASTER); + //join channel + if (0 == pChannel->joinChannel(APP_TOKEN, "", 0, options)) + { + m_btnJoinChannel.EnableWindow(FALSE); + m_cmbChannelList.InsertString(m_cmbChannelList.GetCount(), strChannelName); + strInfo.Format(_T("join channel:%s ...."), strChannelName); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + } +} + + +void CAgoraMultiChannelDlg::OnBnClickedButtonLeaveChannel() +{ + CString strInfo; + int nSel = m_cmbChannelList.GetCurSel(); + if (nSel < 0) { + return; + } + CString strChannelName; + m_cmbChannelList.GetWindowText(strChannelName); + std::string szChannelName = cs2utf8(strChannelName); + bool bFind = false; + + int i = 0; + for (auto & channelInfo : m_channels) + { + if (channelInfo.channelName == szChannelName) + { + //leave other channel + channelInfo.channel->leaveChannel(); + strInfo.Format(_T("leave channel %s"), strChannelName); + bFind = true; + break; + } + i++; + } + if (!bFind) + { + //leave main channel in the engine. + if (0 == m_rtcEngine->leaveChannel()) { + strInfo.Format(_T("leave channel %s"), strChannelName); + } + } + m_cmbChannelList.DeleteString(nSel); + m_cmbChannelList.SetCurSel(nSel - 1 < 0 ? 0 : nSel - 1); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + + +void CAgoraMultiChannelDlg::OnSelchangeListInfoBroadcasting() +{ + int sel = m_lstInfo.GetCurSel(); + if (sel < 0)return; + CString strDetail; + m_lstInfo.GetText(sel, strDetail); + m_staDetail.SetWindowText(strDetail); +} + + +//EID_JOINCHANNEL_SUCCESS message window handler +LRESULT CAgoraMultiChannelDlg::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + IChannel* pChannel = (IChannel*)wParam; + m_joinChannel = true; + m_btnJoinChannel.EnableWindow(TRUE); + CString strInfo; + if (pChannel == 0) + { + strInfo.Format(_T("join :%s success, uid=:%u"), m_strMainChannel, lParam); + m_localVideoWnd.SetUID(lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + else { + for (auto & info:m_channels) + { + if (info.channel == pChannel) + { + strInfo.Format(_T("join :%s success, uid=:%u"),utf82cs(info.channelName), lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + } + } + return 0; +} + +//EID_LEAVEHANNEL_SUCCESS message window handler +LRESULT CAgoraMultiChannelDlg::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + IChannel* pChannel = (IChannel*)wParam; + CString strInfo; + if (pChannel == 0) + { + strInfo.Format(_T("leave %s channel success"), m_strMainChannel); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_joinChannel = false; + } + else { + int i = 0; + for (auto & info:m_channels) + { + if (info.channel == pChannel) + { + strInfo.Format(_T("leave %s channel success"), utf82cs(info.channelName)); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + info.channel->release(); + delete info.evnetHandler; + m_channels.erase(m_channels.begin() + i); + break; + } + i++; + } + } + return 0; +} + +//EID_USER_JOINED message window handler +LRESULT CAgoraMultiChannelDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + IChannel* pChannel = (IChannel*)wParam; + CString strInfo; + if (pChannel == 0) + { + strInfo.Format(_T("%u joined %s"), lParam, m_strMainChannel); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + else { + for (auto & info : m_channels) + { + if (info.channel == pChannel) + { + strInfo.Format(_T("%u joined %s"), lParam, utf82cs(info.channelName)); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + break; + } + } + } + return 0; +} + +//EID_USER_OFFLINE message handler. +LRESULT CAgoraMultiChannelDlg::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + CString strInfo; + IChannel* pChannel = (IChannel*)wParam; + uid_t remoteUid = (uid_t)lParam; + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + + if (pChannel == 0) + { + strInfo.Format(_T("%u offline %s"), remoteUid, m_strMainChannel); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_rtcEngine->setupRemoteVideo(canvas); + } + else { + for (auto & info : m_channels) + { + if (info.channel == pChannel) + { + strInfo.Format(_T("%u offline %s"), remoteUid, utf82cs(info.channelName)); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + break; + } + } + } + return 0; +} + +//EID_REMOTE_VIDEO_STATE_CHANED message window handler. +LRESULT CAgoraMultiChannelDlg::OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam) +{ + PVideoStateStateChanged stateChanged = (PVideoStateStateChanged)wParam; + if (stateChanged) { + //onRemoteVideoStateChanged + CString strSateInfo; + switch (stateChanged->state) { + case REMOTE_VIDEO_STATE_STARTING: + strSateInfo = _T("REMOTE_VIDEO_STATE_STARTING"); + break; + case REMOTE_VIDEO_STATE_STOPPED: + strSateInfo = _T("strSateInfo"); + break; + case REMOTE_VIDEO_STATE_DECODING: + strSateInfo = _T("REMOTE_VIDEO_STATE_DECODING"); + break; + case REMOTE_VIDEO_STATE_FAILED: + strSateInfo = _T("REMOTE_VIDEO_STATE_FAILED "); + break; + case REMOTE_VIDEO_STATE_FROZEN: + strSateInfo = _T("REMOTE_VIDEO_STATE_FROZEN "); + break; + } + CString strInfo; + strInfo.Format(_T("onRemoteVideoStateChanged: uid=%u, %s"), stateChanged->uid, strSateInfo); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + return 0; +} + + + +/* +note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one +parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). +*/ +void CMultiChannelEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)0, (LPARAM)uid); + } +} +/* +note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. +parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). +*/ +void CMultiChannelEventHandler::onUserJoined(uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)0, (LPARAM)uid); + } +} + +/* +note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. +parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. +*/ +void CMultiChannelEventHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), 0, (LPARAM)uid); + } +} +/* +note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. +parameters: + stats: Call statistics. +*/ + +void CMultiChannelEventHandler::onLeaveChannel(const RtcStats& stats) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } +} +/** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. +*/ +void CMultiChannelEventHandler::onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) +{ + if (m_hMsgHanlder) { + PVideoStateStateChanged stateChanged = new VideoStateStateChanged; + stateChanged->uid = uid; + stateChanged->reason = reason; + stateChanged->state = state; + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), (WPARAM)stateChanged, 0); + } +} + + diff --git a/windows/APIExample/APIExample/Advanced/MultiChannel/CAgoraMultiChannelDlg.h b/windows/APIExample/APIExample/Advanced/MultiChannel/CAgoraMultiChannelDlg.h new file mode 100644 index 000000000..588951d36 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/MultiChannel/CAgoraMultiChannelDlg.h @@ -0,0 +1,612 @@ +#pragma once +#include "AGVideoWnd.h" + +class CMultiChannelEventHandler : public IRtcEngineEventHandler +{ +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override; + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override; + /** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. + */ + virtual void onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) override; +private: + HWND m_hMsgHanlder; +}; + + + +class ChannelEventHandler :public agora::rtc::IChannelEventHandler +{ +private: + HWND m_hMsgHanlder; + +public: + + void setMsgHandler(HWND msgHandler) + { + this->m_hMsgHanlder = msgHandler; + + } + + /** Reports the warning code of `IChannel`. + @param rtcChannel IChannel + @param warn The warning code: #WARN_CODE_TYPE + @param msg The warning message. + + */ + virtual void onChannelWarning(IChannel *rtcChannel, int warn, const char* msg) { + } + /** Reports the error code of `IChannel`. + + @param rtcChannel IChannel + @param err The error code: #ERROR_CODE_TYPE + @param msg The error message. + */ + virtual void onChannelError(IChannel *rtcChannel, int err, const char* msg) { + } + /** Occurs when a user joins a channel. + + This callback notifies the application that a user joins a specified channel. + + @param rtcChannel IChannel + @param uid The user ID. If the `uid` is not specified in the \ref IChannel::joinChannel "joinChannel" method, the server automatically assigns a `uid`. + + @param elapsed Time elapsed (ms) from the local user calling \ref IChannel::joinChannel "joinChannel" until this callback is triggered. + */ + virtual void onJoinChannelSuccess(IChannel *rtcChannel, uid_t uid, int elapsed) { + if (m_hMsgHanlder) + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)rtcChannel, uid); + } + /** Occurs when a user rejoins the channel after being disconnected due to network problems. + + @param rtcChannel IChannel + @param uid The user ID. + @param elapsed Time elapsed (ms) from the local user starting to reconnect until this callback is triggered. + + */ + virtual void onRejoinChannelSuccess(IChannel *rtcChannel, uid_t uid, int elapsed) { + } + /** Occurs when a user leaves the channel. + + This callback notifies the application that a user leaves the channel when the application calls the \ref agora::rtc::IChannel::leaveChannel "leaveChannel" method. + + The application retrieves information, such as the call duration and statistics. + + @param rtcChannel IChannel + @param stats The call statistics: RtcStats. + */ + virtual void onLeaveChannel(IChannel *rtcChannel, const RtcStats& stats) { + if (m_hMsgHanlder) + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), (WPARAM)rtcChannel, 0); + } + /** Occurs when the user role switches in the live interactive streaming. For example, from a host to an audience or vice versa. + + This callback notifies the application of a user role switch when the application calls the \ref IChannel::setClientRole "setClientRole" method. + + The SDK triggers this callback when the local user switches the user role by calling the \ref IChannel::setClientRole "setClientRole" method after joining the channel. + + @param rtcChannel IChannel + @param oldRole Role that the user switches from: #CLIENT_ROLE_TYPE. + @param newRole Role that the user switches to: #CLIENT_ROLE_TYPE. + */ + virtual void onClientRoleChanged(IChannel *rtcChannel, CLIENT_ROLE_TYPE oldRole, CLIENT_ROLE_TYPE newRole) { + } + /** Occurs when a remote user (`COMMUNICATION`)/ host (`LIVE_BROADCASTING`) joins the channel. + + - `COMMUNICATION` profile: This callback notifies the application that another user joins the channel. If other users are already in the channel, the SDK also reports to the application on the existing users. + - `LIVE_BROADCASTING` profile: This callback notifies the application that the host joins the channel. If other hosts are already in the channel, the SDK also reports to the application on the existing hosts. We recommend limiting the number of hosts to 17. + + The SDK triggers this callback under one of the following circumstances: + - A remote user/host joins the channel by calling the \ref agora::rtc::IChannel::joinChannel "joinChannel" method. + - A remote user switches the user role to the host by calling the \ref agora::rtc::IChannel::setClientRole "setClientRole" method after joining the channel. + - A remote user/host rejoins the channel after a network interruption. + - The host injects an online media stream into the channel by calling the \ref agora::rtc::IChannel::addInjectStreamUrl "addInjectStreamUrl" method. + + @note In the `LIVE_BROADCASTING` profile: + - The host receives this callback when another host joins the channel. + - The audience in the channel receives this callback when a new host joins the channel. + - When a web application joins the channel, the SDK triggers this callback as long as the web application publishes streams. + + @param rtcChannel IChannel + @param uid User ID of the user or host joining the channel. + @param elapsed Time delay (ms) from the local user calling the \ref IChannel::joinChannel "joinChannel" method until the SDK triggers this callback. + */ + virtual void onUserJoined(IChannel *rtcChannel, uid_t uid, int elapsed) { + if (m_hMsgHanlder) + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)rtcChannel, (LPARAM)uid); + } + /** Occurs when a remote user ( `COMMUNICATION`)/host (`LIVE_BROADCASTING`) leaves the channel. + + Reasons why the user is offline: + + - Leave the channel: When the user/host leaves the channel, the user/host sends a goodbye message. When the message is received, the SDK assumes that the user/host leaves the channel. + - Drop offline: When no data packet of the user or host is received for a certain period of time, the SDK assumes that the user/host drops offline. Unreliable network connections may lead to false detections, so we recommend using the Agora RTM SDK for more reliable offline detection. + + @param rtcChannel IChannel + @param uid User ID of the user leaving the channel or going offline. + @param reason Reason why the user is offline: #USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(IChannel *rtcChannel, uid_t uid, USER_OFFLINE_REASON_TYPE reason) { + if (m_hMsgHanlder) + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)rtcChannel, (LPARAM)uid); + } + /** Occurs when the SDK cannot reconnect to Agora's edge server 10 seconds after its connection to the server is interrupted. + + The SDK triggers this callback when it cannot connect to the server 10 seconds after calling the \ref IChannel::joinChannel "joinChannel" method, whether or not it is in the channel. + + This callback is different from \ref agora::rtc::IRtcEngineEventHandler::onConnectionInterrupted "onConnectionInterrupted": + + - The SDK triggers the `onConnectionInterrupted` callback when it loses connection with the server for more than four seconds after it successfully joins the channel. + - The SDK triggers the `onConnectionLost` callback when it loses connection with the server for more than 10 seconds, whether or not it joins the channel. + + If the SDK fails to rejoin the channel 20 minutes after being disconnected from Agora's edge server, the SDK stops rejoining the channel. + + @param rtcChannel IChannel + */ + virtual void onConnectionLost(IChannel *rtcChannel) { + } + /** Occurs when the token expires. + + After a token is specified by calling the \ref IChannel::joinChannel "joinChannel" method, if the SDK losses connection with the Agora server due to network issues, the token may expire after a certain period of time and a new token may be required to reconnect to the server. + + This callback notifies the app to generate a new token and call `joinChannel` to rejoin the channel with the new token. + + @param rtcChannel IChannel + */ + virtual void onRequestToken(IChannel *rtcChannel) { + } + /** Occurs when the token expires in 30 seconds. + + The user becomes offline if the token used in the \ref IChannel::joinChannel "joinChannel" method expires. The SDK triggers this callback 30 seconds before the token expires to remind the application to get a new token. Upon receiving this callback, generate a new token on the server and call the \ref IChannel::renewToken "renewToken" method to pass the new token to the SDK. + + @param rtcChannel IChannel + @param token Token that expires in 30 seconds. + */ + virtual void onTokenPrivilegeWillExpire(IChannel *rtcChannel, const char* token) { + + } + /** Reports the statistics of the current call. + + The SDK triggers this callback once every two seconds after the user joins the channel. + + @param rtcChannel IChannel + @param stats Statistics of the RtcEngine: RtcStats. + */ + virtual void onRtcStats(IChannel *rtcChannel, const RtcStats& stats) { + + } + /** Reports the last mile network quality of each user in the channel once every two seconds. + + Last mile refers to the connection between the local device and Agora's edge server. This callback reports once every two seconds the last mile network conditions of each user in the channel. If a channel includes multiple users, the SDK triggers this callback as many times. + + @param rtcChannel IChannel + @param uid User ID. The network quality of the user with this @p uid is reported. If @p uid is 0, the local network quality is reported. + @param txQuality Uplink transmission quality rating of the user in terms of the transmission bitrate, packet loss rate, average RTT (Round-Trip Time), and jitter of the uplink network. @p txQuality is a quality rating helping you understand how well the current uplink network conditions can support the selected VideoEncoderConfiguration. For example, a 1000 Kbps uplink network may be adequate for video frames with a resolution of 640 * 480 and a frame rate of 15 fps in the `LIVE_BROADCASTING` profile, but may be inadequate for resolutions higher than 1280 * 720. See #QUALITY_TYPE. + @param rxQuality Downlink network quality rating of the user in terms of the packet loss rate, average RTT, and jitter of the downlink network. See #QUALITY_TYPE. + */ + virtual void onNetworkQuality(IChannel *rtcChannel, uid_t uid, int txQuality, int rxQuality) { + + } + /** Reports the statistics of the video stream from each remote user/host. + * + * The SDK triggers this callback once every two seconds for each remote + * user/host. If a channel includes multiple remote users, the SDK + * triggers this callback as many times. + * + * @param rtcChannel IChannel + * @param stats Statistics of the remote video stream. See + * RemoteVideoStats. + */ + virtual void onRemoteVideoStats(IChannel *rtcChannel, const RemoteVideoStats& stats) { + } + /** Reports the statistics of the audio stream from each remote user/host. + + This callback replaces the \ref agora::rtc::IRtcEngineEventHandler::onAudioQuality "onAudioQuality" callback. + + The SDK triggers this callback once every two seconds for each remote user/host. If a channel includes multiple remote users, the SDK triggers this callback as many times. + + @param rtcChannel IChannel + @param stats The statistics of the received remote audio streams. See RemoteAudioStats. + */ + virtual void onRemoteAudioStats(IChannel *rtcChannel, const RemoteAudioStats& stats) { + + } + /** Occurs when the remote audio state changes. + + This callback indicates the state change of the remote audio stream. + @note This callback does not work properly when the number of users (in the `COMMUNICATION` profile) or hosts (in the `LIVE_BROADCASTING` profile) in the channel exceeds 17. + + @param rtcChannel IChannel + @param uid ID of the remote user whose audio state changes. + @param state State of the remote audio. See #REMOTE_AUDIO_STATE. + @param reason The reason of the remote audio state change. + See #REMOTE_AUDIO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref IChannel::joinChannel "joinChannel" method until the SDK + triggers this callback. + */ + virtual void onRemoteAudioStateChanged(IChannel *rtcChannel, uid_t uid, REMOTE_AUDIO_STATE state, REMOTE_AUDIO_STATE_REASON reason, int elapsed) { + + } + + /** Occurs when the audio publishing state changes. + * + * @since v3.1.0 + * + * This callback indicates the publishing state change of the local audio stream. + * + * @param rtcChannel IChannel + * @param oldState The previous publishing state. For details, see #STREAM_PUBLISH_STATE. + * @param newState The current publishing state. For details, see #STREAM_PUBLISH_STATE. + * @param elapseSinceLastState The time elapsed (ms) from the previous state to the current state. + */ + virtual void onAudioPublishStateChanged(IChannel *rtcChannel, STREAM_PUBLISH_STATE oldState, STREAM_PUBLISH_STATE newState, int elapseSinceLastState) { + + } + + /** Occurs when the video publishing state changes. + * + * @since v3.1.0 + * + * This callback indicates the publishing state change of the local video stream. + * + * @param rtcChannel IChannel + * @param oldState The previous publishing state. For details, see #STREAM_PUBLISH_STATE. + * @param newState The current publishing state. For details, see #STREAM_PUBLISH_STATE. + * @param elapseSinceLastState The time elapsed (ms) from the previous state to the current state. + */ + virtual void onVideoPublishStateChanged(IChannel *rtcChannel, STREAM_PUBLISH_STATE oldState, STREAM_PUBLISH_STATE newState, int elapseSinceLastState) { + + } + + /** Occurs when the audio subscribing state changes. + * + * @since v3.1.0 + * + * This callback indicates the subscribing state change of a remote audio stream. + * + * @param rtcChannel IChannel + * @param uid The ID of the remote user. + * @param oldState The previous subscribing state. For details, see #STREAM_SUBSCRIBE_STATE. + * @param newState The current subscribing state. For details, see #STREAM_SUBSCRIBE_STATE. + * @param elapseSinceLastState The time elapsed (ms) from the previous state to the current state. + */ + virtual void onAudioSubscribeStateChanged(IChannel *rtcChannel, uid_t uid, STREAM_SUBSCRIBE_STATE oldState, STREAM_SUBSCRIBE_STATE newState, int elapseSinceLastState) { + + } + + /** Occurs when the audio subscribing state changes. + * + * @since v3.1.0 + * + * This callback indicates the subscribing state change of a remote video stream. + * + * @param rtcChannel IChannel= + * @param uid The ID of the remote user. + * @param oldState The previous subscribing state. For details, see #STREAM_SUBSCRIBE_STATE. + * @param newState The current subscribing state. For details, see #STREAM_SUBSCRIBE_STATE. + * @param elapseSinceLastState The time elapsed (ms) from the previous state to the current state. + */ + virtual void onVideoSubscribeStateChanged(IChannel *rtcChannel, uid_t uid, STREAM_SUBSCRIBE_STATE oldState, STREAM_SUBSCRIBE_STATE newState, int elapseSinceLastState) { + + } + + /** Reports which user is the loudest speaker. + + If the user enables the audio volume indication by calling the \ref IRtcEngine::enableAudioVolumeIndication(int, int, bool) "enableAudioVolumeIndication" method, this callback returns the @p uid of the active speaker detected by the audio volume detection module of the SDK. + + @note + - To receive this callback, you need to call the \ref IRtcEngine::enableAudioVolumeIndication(int, int, bool) "enableAudioVolumeIndication" method. + - This callback returns the user ID of the user with the highest voice volume during a period of time, instead of at the moment. + + @param rtcChannel IChannel + @param uid User ID of the active speaker. A `uid` of 0 represents the local user. + */ + virtual void onActiveSpeaker(IChannel *rtcChannel, uid_t uid) { + + } + /** Occurs when the video size or rotation of a specified user changes. + + @param rtcChannel IChannel + @param uid User ID of the remote user or local user (0) whose video size or rotation changes. + @param width New width (pixels) of the video. + @param height New height (pixels) of the video. + @param rotation New rotation of the video [0 to 360). + */ + virtual void onVideoSizeChanged(IChannel *rtcChannel, uid_t uid, int width, int height, int rotation) { + + } + /** Occurs when the remote video state changes. + + @note This callback does not work properly when the number of users (in the `COMMUNICATION` profile) or hosts (in the `LIVE_BROADCASTING` profile) in the channel exceeds 17. + + @param rtcChannel IChannel + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IChannel::joinChannel "joinChannel" method until the + SDK triggers this callback. + */ + virtual void onRemoteVideoStateChanged(IChannel *rtcChannel, uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) { + + } + /** Occurs when the local user receives the data stream from the remote user within five seconds. + + The SDK triggers this callback when the local user receives the stream message that the remote user sends by calling the \ref agora::rtc::IChannel::sendStreamMessage "sendStreamMessage" method. + + @param rtcChannel IChannel + @param uid User ID of the remote user sending the message. + @param streamId Stream ID. + @param data The data received by the local user. + @param length Length of the data in bytes. + */ + virtual void onStreamMessage(IChannel *rtcChannel, uid_t uid, int streamId, const char* data, size_t length) { + + } + /** Occurs when the local user does not receive the data stream from the remote user within five seconds. + + The SDK triggers this callback when the local user fails to receive the stream message that the remote user sends by calling the \ref agora::rtc::IChannel::sendStreamMessage "sendStreamMessage" method. + + @param rtcChannel IChannel + @param uid User ID of the remote user sending the message. + @param streamId Stream ID. + @param code Error code: #ERROR_CODE_TYPE. + @param missed Number of lost messages. + @param cached Number of incoming cached messages when the data stream is interrupted. + */ + virtual void onStreamMessageError(IChannel *rtcChannel, uid_t uid, int streamId, int code, int missed, int cached) { + + } + /** Occurs when the state of the media stream relay changes. + * + * The SDK returns the state of the current media relay with any error + * message. + * @param rtcChannel IChannel + * @param state The state code in #CHANNEL_MEDIA_RELAY_STATE. + * @param code The error code in #CHANNEL_MEDIA_RELAY_ERROR. + */ + virtual void onChannelMediaRelayStateChanged(IChannel *rtcChannel, CHANNEL_MEDIA_RELAY_STATE state, CHANNEL_MEDIA_RELAY_ERROR code) { + + } + /** Reports events during the media stream relay. + * @param rtcChannel IChannel + * @param code The event code in #CHANNEL_MEDIA_RELAY_EVENT. + */ + virtual void onChannelMediaRelayEvent(IChannel *rtcChannel, CHANNEL_MEDIA_RELAY_EVENT code) { + + } + /** + Occurs when the state of the RTMP streaming changes. + + The SDK triggers this callback to report the result of the local user calling the \ref agora::rtc::IChannel::addPublishStreamUrl "addPublishStreamUrl" or \ref agora::rtc::IChannel::removePublishStreamUrl "removePublishStreamUrl" method. + + This callback indicates the state of the RTMP streaming. When exceptions occur, you can troubleshoot issues by referring to the detailed error descriptions in the *errCode* parameter. + + @param rtcChannel IChannel + @param url The RTMP URL address. + @param state The RTMP streaming state. See: #RTMP_STREAM_PUBLISH_STATE. + @param errCode The detailed error information for streaming. See: #RTMP_STREAM_PUBLISH_ERROR. + */ + virtual void onRtmpStreamingStateChanged(IChannel *rtcChannel, const char *url, RTMP_STREAM_PUBLISH_STATE state, RTMP_STREAM_PUBLISH_ERROR errCode) { + + } + + /** Reports events during the RTMP streaming. + * + * @since v3.1.0 + * + * @param rtcChannel IChannel + * @param url The RTMP streaming URL. + * @param eventCode The event code. See #RTMP_STREAMING_EVENT + */ + virtual void onRtmpStreamingEvent(IChannel *rtcChannel, const char* url, RTMP_STREAMING_EVENT eventCode) { + + } + + /** Occurs when the publisher's transcoding is updated. + + When the `LiveTranscoding` class in the \ref agora::rtc::IChannel::setLiveTranscoding "setLiveTranscoding" method updates, the SDK triggers the `onTranscodingUpdated` callback to report the update information to the local host. + + @note If you call the `setLiveTranscoding` method to set the LiveTranscoding class for the first time, the SDK does not trigger the `onTranscodingUpdated` callback. + + @param rtcChannel IChannel + */ + virtual void onTranscodingUpdated(IChannel *rtcChannel) { + + } + /** Occurs when a voice or video stream URL address is added to the live interactive streaming. + + @param rtcChannel IChannel + @param url The URL address of the externally injected stream. + @param uid User ID. + @param status State of the externally injected stream: #INJECT_STREAM_STATUS. + */ + virtual void onStreamInjectedStatus(IChannel *rtcChannel, const char* url, uid_t uid, int status) { + + } + /** Occurs when the published media stream falls back to an audio-only stream due to poor network conditions or switches back to the video after the network conditions improve. + + If you call \ref IRtcEngine::setLocalPublishFallbackOption "setLocalPublishFallbackOption" and set *option* as #STREAM_FALLBACK_OPTION_AUDIO_ONLY, the SDK triggers this callback when the published stream falls back to audio-only mode due to poor uplink conditions, or when the audio stream switches back to the video after the uplink network condition improves. + + @param rtcChannel IChannel + @param isFallbackOrRecover Whether the published stream falls back to audio-only or switches back to the video: + - true: The published stream falls back to audio-only due to poor network conditions. + - false: The published stream switches back to the video after the network conditions improve. + */ + virtual void onLocalPublishFallbackToAudioOnly(IChannel *rtcChannel, bool isFallbackOrRecover) { + + } + /** Occurs when the remote media stream falls back to audio-only stream + * due to poor network conditions or switches back to the video stream + * after the network conditions improve. + * + * If you call + * \ref IRtcEngine::setRemoteSubscribeFallbackOption + * "setRemoteSubscribeFallbackOption" and set + * @p option as #STREAM_FALLBACK_OPTION_AUDIO_ONLY, the SDK triggers this + * callback when the remote media stream falls back to audio-only mode due + * to poor uplink conditions, or when the remote media stream switches + * back to the video after the uplink network condition improves. + * + * @note Once the remote media stream switches to the low stream due to + * poor network conditions, you can monitor the stream switch between a + * high and low stream in the RemoteVideoStats callback. + * @param rtcChannel IChannel + * @param uid ID of the remote user sending the stream. + * @param isFallbackOrRecover Whether the remotely subscribed media stream + * falls back to audio-only or switches back to the video: + * - true: The remotely subscribed media stream falls back to audio-only + * due to poor network conditions. + * - false: The remotely subscribed media stream switches back to the + * video stream after the network conditions improved. + */ + virtual void onRemoteSubscribeFallbackToAudioOnly(IChannel *rtcChannel, uid_t uid, bool isFallbackOrRecover) { + + } + /** Occurs when the connection state between the SDK and the server changes. + + @param rtcChannel IChannel + @param state See #CONNECTION_STATE_TYPE. + @param reason See #CONNECTION_CHANGED_REASON_TYPE. + */ + virtual void onConnectionStateChanged(IChannel *rtcChannel, + CONNECTION_STATE_TYPE state, + CONNECTION_CHANGED_REASON_TYPE reason) { + + } +}; + + +struct ChannelInfo +{ + std::string channelName; + IChannel* channel; + IChannelEventHandler* evnetHandler; + + ChannelInfo(std::string channelName_,IChannel* channel_,IChannelEventHandler *eventHandler_): + channelName(channelName_), channel(channel_), evnetHandler(eventHandler_){} +}; + +class CAgoraMultiChannelDlg : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraMultiChannelDlg) + +public: + CAgoraMultiChannelDlg(CWnd* pParent = nullptr); + virtual ~CAgoraMultiChannelDlg(); + + enum { IDD = IDD_DIALOG_MULTI_CHANNEL }; +public: + //Initialize the Ctrl Text. + void InitCtrlText(); + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //render local video from SDK local capture. + void RenderLocalVideo(); + //resume window status + void ResumeStatus(); +private: + bool m_joinChannel = false; + bool m_initialize = false; + bool m_audioMixing = false; + IRtcEngine* m_rtcEngine = nullptr; + CAGVideoWnd m_localVideoWnd; + CMultiChannelEventHandler m_eventHandler; + std::vector m_channels; + CString m_strMainChannel; + +protected: + virtual void DoDataExchange(CDataExchange* pDX); + LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam); + DECLARE_MESSAGE_MAP() +public: + CStatic m_staVideoArea; + CListBox m_lstInfo; + CStatic m_staChannel; + CEdit m_edtChannel; + CButton m_btnJoinChannel; + CStatic m_staChannelList; + CComboBox m_cmbChannelList; + CButton m_btnLeaveChannel; + CStatic m_staDetail; + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + virtual BOOL OnInitDialog(); + virtual BOOL PreTranslateMessage(MSG* pMsg); + afx_msg void OnBnClickedButtonJoinchannel(); + afx_msg void OnBnClickedButtonLeaveChannel(); + afx_msg void OnSelchangeListInfoBroadcasting(); +}; diff --git a/windows/APIExample/APIExample/Advanced/OriginalAudio/CAgoraOriginalAudioDlg.cpp b/windows/APIExample/APIExample/Advanced/OriginalAudio/CAgoraOriginalAudioDlg.cpp new file mode 100644 index 000000000..08eea9808 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/OriginalAudio/CAgoraOriginalAudioDlg.cpp @@ -0,0 +1,528 @@ +#include "stdafx.h" +#include "APIExample.h" +#include "CAgoraOriginalAudioDlg.h" + + + +IMPLEMENT_DYNAMIC(CAgoraOriginalAudioDlg, CDialogEx) + +CAgoraOriginalAudioDlg::CAgoraOriginalAudioDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_ORIGINAL_AUDIO_, pParent) +{ + +} + +CAgoraOriginalAudioDlg::~CAgoraOriginalAudioDlg() +{ +} + +void CAgoraOriginalAudioDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannel); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannel); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_STATIC_ORIGINAL_AUDIO, m_staOriginalAudio); + DDX_Control(pDX, IDC_COMBO_ORIGINAL_AUDIO, m_cmbOriginalAudio); + DDX_Control(pDX, IDC_BUTTON_SET_AUDIO_PROC, m_btnSetAudioProc); + DDX_Control(pDX, IDC_STATIC_DETAIL, m_staDetail); +} + + +BEGIN_MESSAGE_MAP(CAgoraOriginalAudioDlg, CDialogEx) + ON_WM_SHOWWINDOW() + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraOriginalAudioDlg::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraOriginalAudioDlg::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraOriginalAudioDlg::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraOriginalAudioDlg::OnEIDUserOffline) + ON_MESSAGE(WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), &CAgoraOriginalAudioDlg::OnEIDRemoteVideoStateChanged) + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraOriginalAudioDlg::OnBnClickedButtonJoinchannel) + ON_BN_CLICKED(IDC_BUTTON_SET_AUDIO_PROC, &CAgoraOriginalAudioDlg::OnBnClickedButtonSetOriginalProc) + ON_LBN_SELCHANGE(IDC_LIST_INFO_BROADCASTING, &CAgoraOriginalAudioDlg::OnSelchangeListInfoBroadcasting) +END_MESSAGE_MAP() + +/* +* According to the setting of audio collection frame rate, +* the Agora SDK calls this callback function at an appropriate time +* to obtain the audio data collected by the user. +*/ +bool COriginalAudioProcFrameObserver::onRecordAudioFrame(AudioFrame& audioFrame) +{ + SIZE_T nSize = audioFrame.channels * audioFrame.samples * 2; + unsigned int readByte = 0; + int timestamp = GetTickCount(); + short *pBuffer = (short *)audioFrame.buffer; + for (SIZE_T i = 0; i < nSize / 2; i++) + { + if (pBuffer[i] * 2 > 32767) { + pBuffer[i] = 32767; + } + else if (pBuffer[i] * 2 < -32768) { + pBuffer[i] = -32768; + } + else { + pBuffer[i] *= 2; + } + } +#ifdef _DEBUG + CString strInfo; + strInfo.Format(_T("audio Frame buffer size:%d, timestamp:%d \n"), nSize, timestamp); + OutputDebugString(strInfo); + audioFrame.renderTimeMs = timestamp; +#endif + return true; +} +/* + Get the sound played. + parameter: + audioFrame:Audio naked data. + See: AudioFrame + return + True: Buffer data in AudioFrame is valid, the data will be sent; + False: The buffer data in the AudioFrame is invalid and will be discarded. +*/ +bool COriginalAudioProcFrameObserver::onPlaybackAudioFrame(AudioFrame& audioFrame) +{ + return true; +} +/* + Gets the data after recording and playing the voice mix. + annotations: + This method returns only single-channel data. + parameter: + audioFrame Audio naked data. See: AudioFrame + return: + True: Buffer data in AudioFrame is valid, the data will be sent; + False: The buffer data in the AudioFrame is invalid and will be discarded. +*/ +bool COriginalAudioProcFrameObserver::onMixedAudioFrame(AudioFrame& audioFrame) +{ + return true; +} +/* + Gets the specified user's voice before the mix. + parameter: + uid: Specifies the user ID of the user. + audioFrame: Audio naked data. See: AudioFrame. + return: + True: Buffer data in AudioFrame is valid, the data will be sent; + False: The buffer data in the AudioFrame is invalid and will be discarded. +*/ +bool COriginalAudioProcFrameObserver::onPlaybackAudioFrameBeforeMixing(unsigned int uid, AudioFrame& audioFrame) +{ + return true; +} + + +//Initialize the Ctrl Text. +void CAgoraOriginalAudioDlg::InitCtrlText() +{ + m_staChannel.SetWindowText(commonCtrlChannel); + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + m_staOriginalAudio.SetWindowText(OriginalAudioCtrlProc); + m_btnSetAudioProc.SetWindowText(OriginalAudioCtrlSetProc); +} + + +//Initialize the Agora SDK +bool CAgoraOriginalAudioDlg::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //enable video in the engine. + m_rtcEngine->enableAudio(); + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + return true; +} + + +//UnInitialize the Agora SDK +void CAgoraOriginalAudioDlg::UnInitAgora() +{ + if (m_rtcEngine) { + if (m_joinChannel) + //leave channel + m_joinChannel = !m_rtcEngine->leaveChannel(); + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + RegisterAudioFrameObserver(FALSE); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} + +//render local video from SDK local capture. +void CAgoraOriginalAudioDlg::RenderLocalVideo() +{ + if (m_rtcEngine) { + //start preview in the engine. + m_rtcEngine->startPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("startPreview")); + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_localVideoWnd.GetSafeHwnd(); + //setup local video in the engine to canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setupLocalVideo")); + } +} + + +//resume window status +void CAgoraOriginalAudioDlg::ResumeStatus() +{ + InitCtrlText(); + m_staDetail.SetWindowText(_T("")); + m_edtChannel.SetWindowText(_T("")); + m_cmbOriginalAudio.SetCurSel(0); + m_lstInfo.ResetContent(); + m_joinChannel = false; + m_initialize = false; + m_setAudioProc = false; +} + +void CAgoraOriginalAudioDlg::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow)//bShwo is true ,show window + { + InitCtrlText(); + RenderLocalVideo(); + } + else { + ResumeStatus(); + } +} + + +BOOL CAgoraOriginalAudioDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + m_localVideoWnd.MoveWindow(&rcArea); + m_localVideoWnd.ShowWindow(SW_SHOW); + + int i = 0; + m_mapAudioFrame.insert(std::make_pair(_T("amplification"), &m_originalAudioProcFrameObserver)); + m_cmbOriginalAudio.InsertString(i++, _T("amplification")); + ResumeStatus(); + return TRUE; +} + + +BOOL CAgoraOriginalAudioDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} + + +void CAgoraOriginalAudioDlg::OnBnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + CString strInfo; + if (!m_joinChannel) { + CString strChannelName; + m_edtChannel.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + std::string szChannelId = cs2utf8(strChannelName); + //join channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + strInfo.Format(_T("join channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + } + } + else { + //leave channel in the engine. + if (0 == m_rtcEngine->leaveChannel()) { + strInfo.Format(_T("leave channel %s"), getCurrentTime()); + } + } + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + +/* + register or unregister agora audio Frame Observer. +*/ +BOOL CAgoraOriginalAudioDlg::RegisterAudioFrameObserver(BOOL bEnable, IAudioFrameObserver *audioFrameObserver) +{ + agora::util::AutoPtr mediaEngine; + //query interface agora::AGORA_IID_MEDIA_ENGINE in the engine. + mediaEngine.queryInterface(m_rtcEngine, agora::AGORA_IID_MEDIA_ENGINE); + int nRet = 0; + if (mediaEngine.get() == NULL) + return FALSE; + //register audio frame observer. + if (bEnable) + nRet = mediaEngine->registerAudioFrameObserver(audioFrameObserver); + else + //unregister audio frame observer. + nRet = mediaEngine->registerAudioFrameObserver(NULL); + + return nRet == 0 ? TRUE : FALSE; +} + +//setOriginalProc button handler. +void CAgoraOriginalAudioDlg::OnBnClickedButtonSetOriginalProc() +{ + if (!m_setAudioProc) + { + CString strInfo; + CString strAudioProc; + m_cmbOriginalAudio.GetWindowText(strAudioProc); + //register audio frame observer. + RegisterAudioFrameObserver(TRUE, m_mapAudioFrame[strAudioProc]); + m_btnSetAudioProc.SetWindowText(OriginalAudioCtrlUnSetProc); + strInfo.Format(_T("register %s auido frame obsever"), strAudioProc); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + else { + //unregister audio frame observer. + RegisterAudioFrameObserver(FALSE, NULL); + m_btnSetAudioProc.SetWindowText(OriginalAudioCtrlSetProc); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("unregister audio frame observer")); + } + m_setAudioProc = !m_setAudioProc; +} + +//select list item handler +void CAgoraOriginalAudioDlg::OnSelchangeListInfoBroadcasting() +{ + int sel = m_lstInfo.GetCurSel(); + if (sel < 0)return; + CString strDetail; + m_lstInfo.GetText(sel, strDetail); + m_staDetail.SetWindowText(strDetail); +} + + + +//EID_JOINCHANNEL_SUCCESS message window handler +LRESULT CAgoraOriginalAudioDlg::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = true; + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + m_btnJoinChannel.EnableWindow(TRUE); + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_localVideoWnd.SetUID(wParam); + //notify parent window + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); + return 0; +} + +//EID_LEAVEHANNEL_SUCCESS message window handler +LRESULT CAgoraOriginalAudioDlg::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + CString strInfo; + strInfo.Format(_T("leave channel success %s"), getCurrentTime()); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return 0; +} + +//EID_USER_JOINED message window handler +LRESULT CAgoraOriginalAudioDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + CString strInfo; + strInfo.Format(_T("%u joined"), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + +//EID_USER_OFFLINE message handler. +LRESULT CAgoraOriginalAudioDlg::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + m_rtcEngine->setupRemoteVideo(canvas); + CString strInfo; + strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + +//EID_REMOTE_VIDEO_STATE_CHANED message window handler. +LRESULT CAgoraOriginalAudioDlg::OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam) +{ + PVideoStateStateChanged stateChanged = (PVideoStateStateChanged)wParam; + if (stateChanged) { + //onRemoteVideoStateChanged + CString strSateInfo; + switch (stateChanged->state) { + case REMOTE_VIDEO_STATE_STARTING: + strSateInfo = _T("REMOTE_VIDEO_STATE_STARTING"); + break; + case REMOTE_VIDEO_STATE_STOPPED: + strSateInfo = _T("strSateInfo"); + break; + case REMOTE_VIDEO_STATE_DECODING: + strSateInfo = _T("REMOTE_VIDEO_STATE_DECODING"); + break; + case REMOTE_VIDEO_STATE_FAILED: + strSateInfo = _T("REMOTE_VIDEO_STATE_FAILED "); + break; + case REMOTE_VIDEO_STATE_FROZEN: + strSateInfo = _T("REMOTE_VIDEO_STATE_FROZEN "); + break; + } + CString strInfo; + strInfo.Format(_T("onRemoteVideoStateChanged: uid=%u, %s"), stateChanged->uid, strSateInfo); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + return 0; +} + + + +/* +note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one +parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). +*/ +void COriginalAudioEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } +} +/* +note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. +parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). +*/ +void COriginalAudioEventHandler::onUserJoined(uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } +} + +/* +note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. +parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. +*/ +void COriginalAudioEventHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } +} +/* +note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. +parameters: + stats: Call statistics. +*/ + +void COriginalAudioEventHandler::onLeaveChannel(const RtcStats& stats) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } +} +/** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. +*/ +void COriginalAudioEventHandler::onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) +{ + if (m_hMsgHanlder) { + PVideoStateStateChanged stateChanged = new VideoStateStateChanged; + stateChanged->uid = uid; + stateChanged->reason = reason; + stateChanged->state = state; + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), (WPARAM)stateChanged, 0); + } +} diff --git a/windows/APIExample/APIExample/Advanced/OriginalAudio/CAgoraOriginalAudioDlg.h b/windows/APIExample/APIExample/Advanced/OriginalAudio/CAgoraOriginalAudioDlg.h new file mode 100644 index 000000000..82dd9ac75 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/OriginalAudio/CAgoraOriginalAudioDlg.h @@ -0,0 +1,190 @@ +#pragma once +#include "AGVideoWnd.h" + + +class COriginalAudioProcFrameObserver : + public agora::media::IAudioFrameObserver +{ +public: + /* + * According to the setting of audio collection frame rate, + * the Agora SDK calls this callback function at an appropriate time + * to obtain the audio data collected by the user. + */ + virtual bool onRecordAudioFrame(AudioFrame& audioFrame); + /* + Get the sound played. + parameter: + audioFrame:Audio naked data. + See: AudioFrame + return + True: Buffer data in AudioFrame is valid, the data will be sent; + False: The buffer data in the AudioFrame is invalid and will be discarded. + */ + virtual bool onPlaybackAudioFrame(AudioFrame& audioFrame); + /* + Gets the data after recording and playing the voice mix. + annotations: + This method returns only single-channel data. + parameter: + audioFrame Audio naked data. See: AudioFrame + return: + True: Buffer data in AudioFrame is valid, the data will be sent; + False: The buffer data in the AudioFrame is invalid and will be discarded. + */ + virtual bool onMixedAudioFrame(AudioFrame& audioFrame); + /* + Gets the specified user's voice before the mix. + parameter: + uid: Specifies the user ID of the user. + audioFrame: Audio naked data. See: AudioFrame. + return: + True: Buffer data in AudioFrame is valid, the data will be sent; + False: The buffer data in the AudioFrame is invalid and will be discarded. + */ + virtual bool onPlaybackAudioFrameBeforeMixing(unsigned int uid, AudioFrame& audioFrame); +}; + + +class COriginalAudioEventHandler : public IRtcEngineEventHandler +{ +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override; + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override; + /** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. + */ + virtual void onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) override; +private: + HWND m_hMsgHanlder; +}; + + + +class CAgoraOriginalAudioDlg : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraOriginalAudioDlg) + +public: + CAgoraOriginalAudioDlg(CWnd* pParent = nullptr); + virtual ~CAgoraOriginalAudioDlg(); + + enum { + IDD = IDD_DIALOG_ORIGINAL_AUDIO_ + }; +public: + //Initialize the Ctrl Text. + void InitCtrlText(); + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //render local video from SDK local capture. + void RenderLocalVideo(); + //resume window status + void ResumeStatus(); + //register or unregister audio frame observer. + BOOL RegisterAudioFrameObserver(BOOL bEnable, IAudioFrameObserver *audioFrameObserver=NULL); + +private: + bool m_joinChannel = false; + bool m_initialize = false; + bool m_setAudioProc = false; + IRtcEngine* m_rtcEngine = nullptr; + CAGVideoWnd m_localVideoWnd; + COriginalAudioEventHandler m_eventHandler; + COriginalAudioProcFrameObserver m_originalAudioProcFrameObserver; + std::map m_mapAudioFrame; + +protected: + virtual void DoDataExchange(CDataExchange* pDX); + LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam); + DECLARE_MESSAGE_MAP() + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + virtual BOOL OnInitDialog(); + virtual BOOL PreTranslateMessage(MSG* pMsg); + afx_msg void OnBnClickedButtonJoinchannel(); + afx_msg void OnBnClickedButtonSetOriginalProc(); + afx_msg void OnSelchangeListInfoBroadcasting(); + +public: + CStatic m_staVideoArea; + CListBox m_lstInfo; + CStatic m_staChannel; + CEdit m_edtChannel; + CButton m_btnJoinChannel; + CStatic m_staOriginalAudio; + CComboBox m_cmbOriginalAudio; + CButton m_btnSetAudioProc; + CStatic m_staDetail; +}; + diff --git a/windows/APIExample/APIExample/Advanced/OriginalVideo/CAgoraOriginalVideoDlg.cpp b/windows/APIExample/APIExample/Advanced/OriginalVideo/CAgoraOriginalVideoDlg.cpp new file mode 100644 index 000000000..1132c48b6 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/OriginalVideo/CAgoraOriginalVideoDlg.cpp @@ -0,0 +1,644 @@ +#include "stdafx.h" +#include "APIExample.h" +#include "CAgoraOriginalVideoDlg.h" + + + +IMPLEMENT_DYNAMIC(CAgoraOriginalVideoDlg, CDialogEx) + +CAgoraOriginalVideoDlg::CAgoraOriginalVideoDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_ORIGINAL_VIDEO, pParent) +{ + +} + +CAgoraOriginalVideoDlg::~CAgoraOriginalVideoDlg() +{ +} + +//Initialize the Ctrl Text. +void CAgoraOriginalVideoDlg::InitCtrlText() +{ + m_staChannel.SetWindowText(commonCtrlChannel); + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + m_staVideoProc.SetWindowText(OriginalVideoCtrlProc); + m_btnSetVideoProc.SetWindowText(OriginalVideoCtrlSetProc); +} + + +//Initialize the Agora SDK +bool CAgoraOriginalVideoDlg::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //enable video in the engine. + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + return true; +} + + +//UnInitialize the Agora SDK +void CAgoraOriginalVideoDlg::UnInitAgora() +{ + if (m_rtcEngine) { + if (m_joinChannel) + //leave channel + m_joinChannel = !m_rtcEngine->leaveChannel(); + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + RegisterVideoFrameObserver(FALSE); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} + +//render local video from SDK local capture. +void CAgoraOriginalVideoDlg::RenderLocalVideo() +{ + if (m_rtcEngine) { + //start preview in the engine. + m_rtcEngine->startPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("startPreview")); + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_localVideoWnd.GetSafeHwnd(); + //setup local video in the engine to canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setupLocalVideo")); + } +} + + +//resume window status +void CAgoraOriginalVideoDlg::ResumeStatus() +{ + InitCtrlText(); + m_staDetail.SetWindowText(_T("")); + m_edtChannel.SetWindowText(_T("")); + m_cmbVideoProc.SetCurSel(0); + m_lstInfo.ResetContent(); + m_joinChannel = false; + m_initialize = false; + m_setVideoProc = false; +} + +void CAgoraOriginalVideoDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannel); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannel); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); + DDX_Control(pDX, IDC_STATIC_DETAIL, m_staDetail); + DDX_Control(pDX, IDC_STATIC_ORIGINAL_VIDEO_PROC, m_staVideoProc); + DDX_Control(pDX, IDC_COMBO_ORIGINAL_VIDEO_PROC, m_cmbVideoProc); + DDX_Control(pDX, IDC_BUTTON_SET_ORIGINAL_PROC, m_btnSetVideoProc); +} + + +BEGIN_MESSAGE_MAP(CAgoraOriginalVideoDlg, CDialogEx) + ON_WM_SHOWWINDOW() + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraOriginalVideoDlg::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraOriginalVideoDlg::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraOriginalVideoDlg::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraOriginalVideoDlg::OnEIDUserOffline) + ON_MESSAGE(WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), &CAgoraOriginalVideoDlg::OnEIDRemoteVideoStateChanged) + ON_LBN_SELCHANGE(IDC_LIST_INFO_BROADCASTING, &CAgoraOriginalVideoDlg::OnSelchangeListInfoBroadcasting) + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraOriginalVideoDlg::OnBnClickedButtonJoinchannel) + ON_BN_CLICKED(IDC_BUTTON_SET_ORIGINAL_PROC, &CAgoraOriginalVideoDlg::OnBnClickedButtonSetOriginalProc) +END_MESSAGE_MAP() + + + + +void CAgoraOriginalVideoDlg::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow)//bShwo is true ,show window + { + InitCtrlText(); + RenderLocalVideo(); + } + else { + ResumeStatus(); + } +} + + +BOOL CAgoraOriginalVideoDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + m_localVideoWnd.MoveWindow(&rcArea); + m_localVideoWnd.ShowWindow(SW_SHOW); + //insert video frame observer. + int i = 0; + m_cmbVideoProc.InsertString(i++, _T("gray")); + m_mapVideoFrame.insert(std::make_pair(_T("gray"), &m_garyVideoFrameObserver)); + m_cmbVideoProc.InsertString(i++, _T("average filter")); + m_mapVideoFrame.insert(std::make_pair(_T("average filter"), &m_averageFilterVideoFrameObserver)); + ResumeStatus(); + return TRUE; +} + + +/* + register or unregister agora video Frame Observer. +*/ +BOOL CAgoraOriginalVideoDlg::RegisterVideoFrameObserver(BOOL bEnable,IVideoFrameObserver * videoFrameObserver) +{ + agora::util::AutoPtr mediaEngine; + //query interface agora::AGORA_IID_MEDIA_ENGINE in the engine. + mediaEngine.queryInterface(m_rtcEngine, agora::AGORA_IID_MEDIA_ENGINE); + int nRet = 0; + if (mediaEngine.get() == NULL) + return FALSE; + if (bEnable) { + //register agora video frame observer. + nRet = mediaEngine->registerVideoFrameObserver(videoFrameObserver); + } + else { + //unregister agora video frame observer. + nRet = mediaEngine->registerVideoFrameObserver(NULL); + } + return nRet == 0 ? TRUE : FALSE; +} + +//click button handler to join channel or leave channel. +void CAgoraOriginalVideoDlg::OnBnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + CString strInfo; + if (!m_joinChannel) { + CString strChannelName; + m_edtChannel.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + std::string szChannelId = cs2utf8(strChannelName); + //join channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + strInfo.Format(_T("join channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + } + } + else { + //leave channel in the engine. + if (0 == m_rtcEngine->leaveChannel()) { + strInfo.Format(_T("leave channel %s"), getCurrentTime()); + } + } + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + + +//click setOriginalProc button handler to register or unregister video frame observer. +void CAgoraOriginalVideoDlg::OnBnClickedButtonSetOriginalProc() +{ + if (!m_setVideoProc) + { + CString strProc; + CString strInfo; + m_cmbVideoProc.GetWindowText(strProc); + if (strProc.IsEmpty())return; + //register video frame observer from m_mapVideoFrame[strProc]. + RegisterVideoFrameObserver(TRUE, m_mapVideoFrame[strProc]); + strInfo.Format(_T("set process:%s"), strProc); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_btnSetVideoProc.SetWindowText(OriginalVideoCtrlUnSetProc); + } + else { + //resume video frame observer. + RegisterVideoFrameObserver(FALSE); + m_btnSetVideoProc.SetWindowText(OriginalVideoCtrlSetProc); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("cancel the process")); + } + m_setVideoProc = !m_setVideoProc; +} + + + +//see the header file for details +bool CGrayVideoProcFrameObserver::onCaptureVideoFrame(VideoFrame & videoFrame) +{ + int nSize = videoFrame.height * videoFrame.width; + //set UV to 128 to mask color information + memset(videoFrame.uBuffer, 128, nSize / 4); + memset(videoFrame.vBuffer, 128, nSize / 4); + return true; +} + +//see the header file for details +bool CGrayVideoProcFrameObserver::onRenderVideoFrame(unsigned int uid, VideoFrame & videoFrame) +{ + return true; +} + +//see the header file for details +bool CAverageFilterVideoProcFrameObserver::onCaptureVideoFrame(VideoFrame & videoFrame) +{ + static int step = 1; + static bool flag = true; + if (flag) + { + step += 2; + } + else { + step -= 2; + } + if (step >= 151) + { + flag = false; + step -= 4; + } + else if (step <= 0) { + flag = true; + step += 4; + } + AverageFiltering((unsigned char *)videoFrame.yBuffer, videoFrame.width, videoFrame.height, step); + AverageFiltering((unsigned char *)videoFrame.uBuffer, videoFrame.width / 2, videoFrame.height / 2, step); + AverageFiltering((unsigned char *)videoFrame.vBuffer, videoFrame.width / 2, videoFrame.height / 2, step); + return true; +} + +//see the header file for details +bool CAverageFilterVideoProcFrameObserver::onRenderVideoFrame(unsigned int uid, VideoFrame & videoFrame) +{ + return true; +} + +static inline +int countSize(int row, int col, int width, int height, int r) { + int x, y; + if (row + r >= height) + x = r * 2 + 1 - (row + r - height + 1); + else if (row < r) + x = r + row + 1; + else + x = r * 2 + 1; + if (col + r >= width) + y = r * 2 + 1 - (col + r - width + 1); + else if (col < r) + y = r + col + 1; + else + y = r * 2 + 1; + return x * y; +} +int dataBuffers[1920][1080][2]; +//average filtering algorithm +void CAverageFilterVideoProcFrameObserver::AverageFiltering(unsigned char * data, int width, int height, int step) +{ + memset(dataBuffers, 0, sizeof(dataBuffers)); + if (step > width || step > height) + return; + if (step == 1)return; + int halfstep = step / 2; + for (int i = 0; i < height; i++) + { + for (int j = 0; j <= halfstep; j++) + { + dataBuffers[i][0][0] += data[i*width + j]; + } + } + + for (int i = 0; i < width; i++) + { + for (int j = 0; j <= halfstep; j++) + { + dataBuffers[0][i][1] += data[j*width + i]; + } + } + + for (int i = 0; i < height; i++) + { + for (int j = 1; j < width; j++) + { + if (j <= halfstep) + { + dataBuffers[i][j][0] = dataBuffers[i][j - 1][0] + data[i*width + j + halfstep]; + } + else { + dataBuffers[i][j][0] = dataBuffers[i][j - 1][0] - data[i*width + j - halfstep - 1] + (j + halfstep < width ? data[i*width + j + halfstep] : 0); + } + } + } + + for (int i = 1; i < height; i++) + { + for (int j = 0; j < width; j++) + { + if (i <= halfstep) + { + dataBuffers[i][j][1] = dataBuffers[i - 1][j][1] + data[(i + halfstep)*width + j]; + } + else { + dataBuffers[i][j][1] = dataBuffers[i - 1][j][1] - data[(i - halfstep - 1)*width + j] + (i + halfstep < height ? data[(i + halfstep)*width + j] : 0); + } + } + } + + int ans = 0; + int prev = 0; + for (int i = 0; i <= halfstep; i++) + { + ans += dataBuffers[i][0][0]; + } + prev = ans; + data[0] = ans / ((halfstep + 1)*(halfstep + 1)); + + for (int i = 0; i < height; i++) + { + for (int j = 0; j < width; j++) + { + if (i == 0 && j == 0)continue; + if (j == 0) + { + if (i > halfstep && i < height - halfstep) + { + ans = prev + + dataBuffers[i + halfstep][0][0] - + dataBuffers[i - halfstep - 1][0][0]; + prev = ans; + data[i*width + j] = ans / countSize(i, j, width, height, halfstep); + } + else if (i <= halfstep) + { + ans = prev + + dataBuffers[i + halfstep][0][0]; + prev = ans; + data[i*width + j] = ans / countSize(i, j, width, height, halfstep); + } + else { + ans = prev - + dataBuffers[i - halfstep - 1][0][0]; + prev = ans; + data[i*width + j] = ans / countSize(i, j, width, height, halfstep); + } + } + else if (j + halfstep >= width) { + ans -= dataBuffers[i][j - halfstep - 1][1]; + data[i*width + j] = ans / countSize(i, j, width, height, halfstep); + } + else if (j < halfstep) { + ans += dataBuffers[i][j + halfstep][1]; + data[i*width + j] = ans / countSize(i, j, width, height, halfstep); + } + else + { + ans += dataBuffers[i][j + halfstep][1] - dataBuffers[i][j - halfstep - 1][1]; + data[i*width + j] = ans / countSize(i, j, width, height, halfstep); + } + } + } +} + + + +//EID_JOINCHANNEL_SUCCESS message window handler +LRESULT CAgoraOriginalVideoDlg::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = true; + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + m_btnJoinChannel.EnableWindow(TRUE); + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_localVideoWnd.SetUID(wParam); + //notify parent window + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); + return 0; +} + +//EID_LEAVEHANNEL_SUCCESS message window handler +LRESULT CAgoraOriginalVideoDlg::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + CString strInfo; + strInfo.Format(_T("leave channel success %s"), getCurrentTime()); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return 0; +} + +//EID_USER_JOINED message window handler +LRESULT CAgoraOriginalVideoDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + CString strInfo; + strInfo.Format(_T("%u joined"), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + +//EID_USER_OFFLINE message handler. +LRESULT CAgoraOriginalVideoDlg::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + m_rtcEngine->setupRemoteVideo(canvas); + CString strInfo; + strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + +//EID_REMOTE_VIDEO_STATE_CHANED message window handler. +LRESULT CAgoraOriginalVideoDlg::OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam) +{ + PVideoStateStateChanged stateChanged = (PVideoStateStateChanged)wParam; + if (stateChanged) { + //onRemoteVideoStateChanged + CString strSateInfo; + switch (stateChanged->state) { + case REMOTE_VIDEO_STATE_STARTING: + strSateInfo = _T("REMOTE_VIDEO_STATE_STARTING"); + break; + case REMOTE_VIDEO_STATE_STOPPED: + strSateInfo = _T("strSateInfo"); + break; + case REMOTE_VIDEO_STATE_DECODING: + strSateInfo = _T("REMOTE_VIDEO_STATE_DECODING"); + break; + case REMOTE_VIDEO_STATE_FAILED: + strSateInfo = _T("REMOTE_VIDEO_STATE_FAILED "); + break; + case REMOTE_VIDEO_STATE_FROZEN: + strSateInfo = _T("REMOTE_VIDEO_STATE_FROZEN "); + break; + } + CString strInfo; + strInfo.Format(_T("onRemoteVideoStateChanged: uid=%u, %s"), stateChanged->uid, strSateInfo); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + return 0; +} + + + +/* +note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one +parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). +*/ +void COriginalVideoEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } +} +/* +note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. +parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). +*/ +void COriginalVideoEventHandler::onUserJoined(uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } +} + +/* +note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. +parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. +*/ +void COriginalVideoEventHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } +} +/* +note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. +parameters: + stats: Call statistics. +*/ + +void COriginalVideoEventHandler::onLeaveChannel(const RtcStats& stats) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } +} +/** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. +*/ +void COriginalVideoEventHandler::onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) +{ + if (m_hMsgHanlder) { + PVideoStateStateChanged stateChanged = new VideoStateStateChanged; + stateChanged->uid = uid; + stateChanged->reason = reason; + stateChanged->state = state; + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), (WPARAM)stateChanged, 0); + } +} + + + + +BOOL CAgoraOriginalVideoDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} + + + +//show details information +void CAgoraOriginalVideoDlg::OnSelchangeListInfoBroadcasting() +{ + int sel = m_lstInfo.GetCurSel(); + if (sel < 0)return; + CString strDetail; + m_lstInfo.GetText(sel, strDetail); + m_staDetail.SetWindowText(strDetail); +} + diff --git a/windows/APIExample/APIExample/Advanced/OriginalVideo/CAgoraOriginalVideoDlg.h b/windows/APIExample/APIExample/Advanced/OriginalVideo/CAgoraOriginalVideoDlg.h new file mode 100644 index 000000000..a044a2522 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/OriginalVideo/CAgoraOriginalVideoDlg.h @@ -0,0 +1,237 @@ +#pragma once +#include "AGVideoWnd.h" + + +// Video gray scale processing Frame Observer +class CGrayVideoProcFrameObserver : + public agora::media::IVideoFrameObserver +{ +public: + virtual ~CGrayVideoProcFrameObserver() { } + /* + Obtain video data from the local camera.After successfully registering + a video data observer, the SDK triggers this callback when each video + frame is captured. You can retrieve the video data from the local camera + in the callback, and then pre-process the video data according to the needs + of the scene.After the preprocessing is done, you can send the processed + video data back to the SDK in this callback. + annotations: + If the video data type you get is RGBA, Agora does not support sending the + processed RGBA data back to the SDK through this callback. + parameter: + videoFrame :VideoFramedata, see VideoFrame for more details + return If the video pre-processing fails,whether to ignore the video frame: + True: No ignore. + False: Ignored, the frame data is not sent back to the SDK. + */ + virtual bool onCaptureVideoFrame(VideoFrame& videoFrame); + /* + Gets video data sent remotely.After successfully registering a video data observer, + the SDK triggers this callback when each video frame is captured. You can retrieve + the video data sent remotely in the callback, and then post-process the video data + according to the scenario requirements.After the post-processing, you can send the + processed video data back to the SDK in the callback. + annotations: + If the video data type you get is RGBA, Agora does not support sending the processed RGBA data back + to the SDK through this callback. + parameter: + uid: The remote user ID to send the frame video + videoFrame: VideoFrame data, see VideoFrame for more details + return If the video pre-processing fails,whether to ignore the video frame: + True: No ignore. + False: Ignored, the frame data is not sent back to the SDK. + */ + virtual bool onRenderVideoFrame(unsigned int uid, VideoFrame& videoFrame); +}; + + +// Video gray scale processing Frame Observer +class CAverageFilterVideoProcFrameObserver : + public agora::media::IVideoFrameObserver +{ +private: + unsigned char * m_buffer = new unsigned char[1920 * 1280 * 3]; + unsigned char * m_height = new unsigned char[1920]; +public: + + virtual ~CAverageFilterVideoProcFrameObserver() { delete[]m_buffer; delete[]m_height; } + /* + Obtain video data from the local camera.After successfully registering + a video data observer, the SDK triggers this callback when each video + frame is captured. You can retrieve the video data from the local camera + in the callback, and then pre-process the video data according to the needs + of the scene.After the preprocessing is done, you can send the processed + video data back to the SDK in this callback. + annotations: + If the video data type you get is RGBA, Agora does not support sending the + processed RGBA data back to the SDK through this callback. + parameter: + videoFrame :VideoFramedata, see VideoFrame for more details + return If the video pre-processing fails,whether to ignore the video frame: + True: No ignore. + False: Ignored, the frame data is not sent back to the SDK. + */ + virtual bool onCaptureVideoFrame(VideoFrame& videoFrame); + /* + Gets video data sent remotely.After successfully registering a video data observer, + the SDK triggers this callback when each video frame is captured. You can retrieve + the video data sent remotely in the callback, and then post-process the video data + according to the scenario requirements.After the post-processing, you can send the + processed video data back to the SDK in the callback. + annotations: + If the video data type you get is RGBA, Agora does not support sending the processed RGBA data back + to the SDK through this callback. + parameter: + uid: The remote user ID to send the frame video + videoFrame: VideoFrame data, see VideoFrame for more details + return If the video pre-processing fails,whether to ignore the video frame: + True: No ignore. + False: Ignored, the frame data is not sent back to the SDK. + */ + virtual bool onRenderVideoFrame(unsigned int uid, VideoFrame& videoFrame); + void AverageFiltering(unsigned char * data, int width, int height, int step); +}; + + +class COriginalVideoEventHandler : public IRtcEngineEventHandler +{ +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override; + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override; + /** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. + */ + virtual void onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) override; +private: + HWND m_hMsgHanlder; +}; + + + +class CAgoraOriginalVideoDlg : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraOriginalVideoDlg) + +public: + CAgoraOriginalVideoDlg(CWnd* pParent = nullptr); + virtual ~CAgoraOriginalVideoDlg(); + + enum { IDD = IDD_DIALOG_ORIGINAL_VIDEO }; +public: + //Initialize the Ctrl Text. + void InitCtrlText(); + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //render local video from SDK local capture. + void RenderLocalVideo(); + //resume window status + void ResumeStatus(); + //register or unregister agora video Frame Observer. + BOOL RegisterVideoFrameObserver(BOOL bEnable, IVideoFrameObserver * videoFrameObserver = NULL); + + + + +private: + bool m_joinChannel = false; + bool m_initialize = false; + bool m_setVideoProc = false; + IRtcEngine* m_rtcEngine = nullptr; + CAGVideoWnd m_localVideoWnd; + COriginalVideoEventHandler m_eventHandler; + + CGrayVideoProcFrameObserver m_garyVideoFrameObserver; + CAverageFilterVideoProcFrameObserver m_averageFilterVideoFrameObserver; + std::map m_mapVideoFrame; +protected: + virtual void DoDataExchange(CDataExchange* pDX); + LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam); + DECLARE_MESSAGE_MAP() +public: + CStatic m_staVideoArea; + CStatic m_staChannel; + CEdit m_edtChannel; + CButton m_btnJoinChannel; + CListBox m_lstInfo; + CStatic m_staDetail; + CStatic m_staVideoProc; + CComboBox m_cmbVideoProc; + CButton m_btnSetVideoProc; + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + virtual BOOL OnInitDialog(); + virtual BOOL PreTranslateMessage(MSG* pMsg); + afx_msg void OnSelchangeListInfoBroadcasting(); + afx_msg void OnBnClickedButtonJoinchannel(); + afx_msg void OnBnClickedButtonSetOriginalProc(); +}; diff --git a/windows/APIExample/APIExample/Advanced/PreCallTest/CAgoraPreCallTestDlg.cpp b/windows/APIExample/APIExample/Advanced/PreCallTest/CAgoraPreCallTestDlg.cpp new file mode 100644 index 000000000..ab90f4843 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/PreCallTest/CAgoraPreCallTestDlg.cpp @@ -0,0 +1,382 @@ +#include "stdafx.h" +#include "APIExample.h" +#include "CAgoraPreCallTestDlg.h" + + + +IMPLEMENT_DYNAMIC(CAgoraPreCallTestDlg, CDialogEx) + +CAgoraPreCallTestDlg::CAgoraPreCallTestDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_PERCALL_TEST, pParent) +{ + +} + +CAgoraPreCallTestDlg::~CAgoraPreCallTestDlg() +{ +} + +void CAgoraPreCallTestDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATIC_ADUIO_INPUT, m_staAudioInput); + DDX_Control(pDX, IDC_STATIC_ADUIO_INPUT_VOL, m_staAudioInputVol); + DDX_Control(pDX, IDC_STATIC_ADUIO_SCENARIO, m_staAudioOutput); + DDX_Control(pDX, IDC_STATIC_ADUIO_OUTPUT_VOL, m_staAudioOutputVol); + DDX_Control(pDX, IDC_STATIC_CAMERA, m_staVideo); + DDX_Control(pDX, IDC_COMBO_VIDEO, m_cmbVideo); + DDX_Control(pDX, IDC_COMBO_AUDIO_INPUT, m_cmbAudioInput); + DDX_Control(pDX, IDC_COMBO_AUDIO_OUTPUT, m_cmbAudioOutput); + DDX_Control(pDX, IDC_SLIDER_INPUT_VOL, m_sldAudioInputVol); + DDX_Control(pDX, IDC_SLIDER_OUTPUT_VOL, m_sldAudioOutputVol); + DDX_Control(pDX, IDC_BUTTON_AUDIO_INPUT_TEST, m_btnAudioInputTest); + DDX_Control(pDX, IDC_BUTTON_AUDIO_OUTPUT_TEST, m_btnAudioOutputTest); + DDX_Control(pDX, IDC_BUTTON_CAMERA, m_btnVideoTest); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); + DDX_Control(pDX, IDC_STATIC_DETAIL, m_staDetails); +} + + +BEGIN_MESSAGE_MAP(CAgoraPreCallTestDlg, CDialogEx) + ON_WM_SHOWWINDOW() + ON_BN_CLICKED(IDC_BUTTON_AUDIO_INPUT_TEST, &CAgoraPreCallTestDlg::OnBnClickedButtonAudioInputTest) + ON_BN_CLICKED(IDC_BUTTON_AUDIO_OUTPUT_TEST, &CAgoraPreCallTestDlg::OnBnClickedButtonAudioOutputTest) + ON_BN_CLICKED(IDC_BUTTON_CAMERA, &CAgoraPreCallTestDlg::OnBnClickedButtonCamera) + ON_LBN_SELCHANGE(IDC_LIST_INFO_BROADCASTING, &CAgoraPreCallTestDlg::OnSelchangeListInfoBroadcasting) + ON_NOTIFY(NM_RELEASEDCAPTURE, IDC_SLIDER_INPUT_VOL, &CAgoraPreCallTestDlg::OnReleasedcaptureSliderInputVol) + ON_NOTIFY(NM_RELEASEDCAPTURE, IDC_SLIDER_OUTPUT_VOL, &CAgoraPreCallTestDlg::OnReleasedcaptureSliderOutputVol) + ON_MESSAGE(WM_MSGID(EID_LASTMILE_PROBE_RESULT), &CAgoraPreCallTestDlg::OnEIDLastmileProbeResult) + ON_MESSAGE(WM_MSGID(EID_LASTMILE_QUAILTY), &CAgoraPreCallTestDlg::OnEIDLastmileQuality) + ON_MESSAGE(WM_MSGID(EID_AUDIO_VOLUME_INDICATION), &CAgoraPreCallTestDlg::OnEIDAudioVolumeIndication) + ON_WM_PAINT() +END_MESSAGE_MAP() + +//init ctrl text. +void CAgoraPreCallTestDlg::InitCtrlText() +{ + m_staVideo.SetWindowText(PerCallTestCtrlCamera); + m_staAudioInput.SetWindowText(PerCallTestCtrlAudioInput); + m_staAudioOutput.SetWindowText(PerCallTestCtrlAudioOutput); + m_staAudioInputVol.SetWindowText(PerCallTestCtrlAudioVol); + m_staAudioOutputVol.SetWindowText(PerCallTestCtrlAudioVol); + m_btnAudioInputTest.SetWindowText(PerCallTestCtrlStartTest); + m_btnAudioOutputTest.SetWindowText(PerCallTestCtrlStartTest); + m_btnVideoTest.SetWindowText(PerCallTestCtrlStartTest); +} + +//Initialize the Agora SDK +bool CAgoraPreCallTestDlg::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize rtc engine")); + LastmileProbeConfig config; + config.probeUplink = true; + config.probeDownlink = true; + config.expectedUplinkBitrate = 100000; + config.expectedDownlinkBitrate = 100000; + //start last mile probe test. + m_rtcEngine->startLastmileProbeTest(config); + m_rtcEngine->enableAudio(); + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("startLastmileProbeTest")); + //create audio and video device manager. + m_audioDeviceManager = new AAudioDeviceManager(m_rtcEngine); + m_videoDeviceManager = new AVideoDeviceManager(m_rtcEngine); + return true; +} + +void CAgoraPreCallTestDlg::UnInitAgora() +{ + if (m_rtcEngine) { + //release device manager. + m_audioDeviceManager->release(); + m_videoDeviceManager->release(); + m_rtcEngine->stopLastmileProbeTest(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopLastmileProbeTest")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} + + + +//resume status. +void CAgoraPreCallTestDlg::ResumeStatus() +{ + InitCtrlText(); + m_netQuality = 0; + m_lstInfo.ResetContent(); + m_cmbAudioOutput.ResetContent(); + m_cmbAudioInput.ResetContent(); + m_cmbVideo.ResetContent(); + m_mapAudioInput.clear(); + m_mapAudioOutput.clear(); + m_mapCamera.clear(); + m_cameraTest = false; + m_audioInputTest = false; + m_audioOutputTest = false; +} + + +void CAgoraPreCallTestDlg::UpdateViews() +{ + char szDeviceName[1024]; + char szDeviceId[1024]; + + m_cmbAudioInput.ResetContent(); + m_cmbAudioOutput.ResetContent(); + m_cmbVideo.ResetContent(); + int nVol; + (*m_audioDeviceManager)->getPlaybackDeviceVolume(&nVol); + m_sldAudioOutputVol.SetPos(nVol); + (*m_audioDeviceManager)->getRecordingDeviceVolume(&nVol); + m_sldAudioInputVol.SetPos(nVol); + //get audio record devices and add to combobox and insert map. + IAudioDeviceCollection *audioRecordDevices = (*m_audioDeviceManager)->enumerateRecordingDevices(); + for (int i = 0; i < audioRecordDevices->getCount(); i++) + { + int nRet = audioRecordDevices->getDevice(i, szDeviceName, szDeviceId); + m_cmbAudioInput.AddString(utf82cs(szDeviceName)); + m_mapAudioInput.insert(std::make_pair(utf82cs(szDeviceName), szDeviceId)); + } + audioRecordDevices->release(); + m_cmbAudioInput.SetCurSel(0); + //get audio playback devices and add to combobox and insert map. + IAudioDeviceCollection *audioPlaybackDevices = (*m_audioDeviceManager)->enumeratePlaybackDevices(); + for (int i = 0; i < audioPlaybackDevices->getCount(); i++) + { + int nRet = audioPlaybackDevices->getDevice(i, szDeviceName, szDeviceId); + m_cmbAudioOutput.AddString(utf82cs(szDeviceName)); + m_mapAudioOutput.insert(std::make_pair(utf82cs(szDeviceName), szDeviceId)); + } + audioPlaybackDevices->release(); + m_cmbAudioOutput.SetCurSel(0); + + //get camera devices and add to combobox and insert map. + auto cameraDevices = (*m_videoDeviceManager)->enumerateVideoDevices(); + for (int i = 0; i < cameraDevices->getCount(); i++) + { + int nRet = cameraDevices->getDevice(i, szDeviceName, szDeviceId); + m_cmbVideo.AddString(utf82cs(szDeviceName)); + m_mapCamera.insert(std::make_pair(utf82cs(szDeviceName), szDeviceId)); + } + m_cmbVideo.SetCurSel(0); + cameraDevices->release(); +} + + + +void CAgoraPreCallTestDlg::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow) + { + InitCtrlText(); + UpdateViews(); + } + else { + ResumeStatus(); + } +} + + +BOOL CAgoraPreCallTestDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + RECT rcArea; + m_staVideoArea.GetWindowRect(&rcArea); + CBitmap bmpNetQuality; + bmpNetQuality.LoadBitmap(IDB_BITMAP_NETWORK_STATE); + m_imgNetQuality.Create(32, 32, ILC_COLOR24 | ILC_MASK, 6, 1); + m_imgNetQuality.Add(&bmpNetQuality, RGB(0xFF, 0, 0xFF)); + m_sldAudioInputVol.SetRange(0, 255); + m_sldAudioOutputVol.SetRange(0, 255); + m_VideoTest.Create(NULL, NULL, WS_CHILD | WS_VISIBLE, CRect(0, 0, 1, 1), this, NULL); + m_VideoTest.MoveWindow(&rcArea); + m_VideoTest.SetVolRange(100); + ResumeStatus(); + return TRUE; +} + +//last mile quality notify +LRESULT CAgoraPreCallTestDlg::OnEIDLastmileQuality(WPARAM wparam, LPARAM lparam) +{ + int quality = wparam; + m_netQuality = quality; + CString strInfo; + strInfo.Format(_T("current network quality:%d"), quality); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + RECT rc = { 16,40,100,100 }; + this->InvalidateRect(&rc); + return TRUE; +} + +LRESULT CAgoraPreCallTestDlg::OnEIDLastmileProbeResult(WPARAM wparam, LPARAM lparam) +{ + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("OnLastmileProbeResult")); + return TRUE; +} + +//audio volume indication message handler +LRESULT CAgoraPreCallTestDlg::OnEIDAudioVolumeIndication(WPARAM wparam, LPARAM lparam) +{ + //set audio volume to show test window. + m_VideoTest.SetCurVol(wparam); + return TRUE; +} + + + +BOOL CAgoraPreCallTestDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} + + +void CAgoraPreCallTestDlg::OnBnClickedButtonAudioInputTest() +{ + int nSel = m_cmbAudioInput.GetCurSel(); + if (nSel < 0)return; + CString strAudioInputName; + m_cmbAudioInput.GetWindowText(strAudioInputName); + if (!m_audioInputTest) + { + //set audio recording device with device id. + (*m_audioDeviceManager)->setRecordingDevice(m_mapAudioInput[strAudioInputName].c_str()); + //enable audio volume indication + m_rtcEngine->enableAudioVolumeIndication(1000, 10, true); + //start audio recording device test + (*m_audioDeviceManager)->startRecordingDeviceTest(1000); + m_btnAudioInputTest.SetWindowText(PerCallTestCtrlStopTest); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("start audio recording device test.")); + } + else { + //stop audio recording device test. + (*m_audioDeviceManager)->stopRecordingDeviceTest(); + //disable audio volume indication. + m_rtcEngine->enableAudioVolumeIndication(1000, 10, false); + m_btnAudioInputTest.SetWindowText(PerCallTestCtrlStartTest); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stop audio recording device test.")); + } + m_audioInputTest = !m_audioInputTest; +} + + + +void CAgoraPreCallTestDlg::OnBnClickedButtonAudioOutputTest() +{ + TCHAR szWavPath[MAX_PATH]; + int nSel = m_cmbAudioInput.GetCurSel(); + if (nSel < 0)return; + CString strAudioInputName; + m_cmbAudioInput.GetWindowText(strAudioInputName); + if (!m_audioOutputTest) + { + ::GetModuleFileName(NULL, szWavPath, MAX_PATH); + LPTSTR lpLastSlash = (LPTSTR)_tcsrchr(szWavPath, _T('\\')) + 1; + _tcscpy_s(lpLastSlash, 16, _T("test.wav")); + SaveResourceToFile(_T("WAVE"), IDR_TEST_WAVE, szWavPath); + //set audio playback device with device id. + (*m_audioDeviceManager)->setPlaybackDevice(m_mapAudioInput[strAudioInputName].c_str()); + //start audio playback device test with wav file path. +#ifdef UNICODE + CHAR szWavPathA[MAX_PATH]; + ::WideCharToMultiByte(CP_ACP, 0, szWavPath, -1, szWavPathA, MAX_PATH, NULL, NULL); + (*m_audioDeviceManager)->startPlaybackDeviceTest(szWavPathA); +#else + (*m_audioDeviceManager)->startPlaybackDeviceTest(szWavPathA); +#endif + m_btnAudioOutputTest.SetWindowText(PerCallTestCtrlStopTest); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("start audio playback device test.")); + } + else { + //stop audio playback device test. + (*m_audioDeviceManager)->stopPlaybackDeviceTest(); + m_btnAudioOutputTest.SetWindowText(PerCallTestCtrlStartTest); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stop audio playback device test. ")); + } + m_audioOutputTest = !m_audioOutputTest; +} + + +void CAgoraPreCallTestDlg::OnBnClickedButtonCamera() +{ + int nSel = m_cmbAudioInput.GetCurSel(); + if (nSel < 0)return; + CString strCamereaDeivce; + m_cmbVideo.GetWindowText(strCamereaDeivce); + if (!m_cameraTest) + { + //set camera device with device id. + (*m_videoDeviceManager)->setDevice(m_mapCamera[strCamereaDeivce].c_str()); + //start camera device test. + (*m_videoDeviceManager)->startDeviceTest(m_VideoTest.GetVideoSafeHwnd()); + m_btnVideoTest.SetWindowText(PerCallTestCtrlStopTest); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("start camera device test. ")); + + } + else { + //stop camera device test. + (*m_videoDeviceManager)->stopDeviceTest(); + m_btnVideoTest.SetWindowText(PerCallTestCtrlStartTest); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stop camera device test. ")); + } + m_cameraTest = !m_cameraTest; +} + + +void CAgoraPreCallTestDlg::OnSelchangeListInfoBroadcasting() +{ + int sel = m_lstInfo.GetCurSel(); + if (sel < 0)return; + CString strDetail; + m_lstInfo.GetText(sel, strDetail); + m_staDetails.SetWindowText(strDetail); +} + + +void CAgoraPreCallTestDlg::OnReleasedcaptureSliderInputVol(NMHDR *pNMHDR, LRESULT *pResult) +{ + LPNMCUSTOMDRAW pNMCD = reinterpret_cast(pNMHDR); + int vol = m_sldAudioInputVol.GetPos(); + //set audio record device volume + (*m_audioDeviceManager)->setRecordingDeviceVolume(vol); + *pResult = 0; +} + + +void CAgoraPreCallTestDlg::OnReleasedcaptureSliderOutputVol(NMHDR *pNMHDR, LRESULT *pResult) +{ + LPNMCUSTOMDRAW pNMCD = reinterpret_cast(pNMHDR); + int vol = m_sldAudioOutputVol.GetPos(); + //set audio playback device volume + (*m_audioDeviceManager)->setPlaybackDeviceVolume(vol); + *pResult = 0; +} + + +void CAgoraPreCallTestDlg::OnPaint() +{ + CPaintDC dc(this); + //draw quality bitmap + m_imgNetQuality.Draw(&dc, m_netQuality, CPoint(16, 40), ILD_NORMAL); +} diff --git a/windows/APIExample/APIExample/Advanced/PreCallTest/CAgoraPreCallTestDlg.h b/windows/APIExample/APIExample/Advanced/PreCallTest/CAgoraPreCallTestDlg.h new file mode 100644 index 000000000..e9d45a1b9 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/PreCallTest/CAgoraPreCallTestDlg.h @@ -0,0 +1,137 @@ +#pragma once +#include "AGVideoTestWnd.h" + +class CAgoraPreCallTestEvnetHandler :public IRtcEngineEventHandler +{ +public: + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + + /** Reports which users are speaking, the speakers' volume and whether the local user is speaking. + This callback reports the IDs and volumes of the loudest speakers (at most 3 users) at the moment in the channel, and whether the local user is speaking. + By default, this callback is disabled. You can enable it by calling the \ref IRtcEngine::enableAudioVolumeIndication(int, int, bool) "enableAudioVolumeIndication" method. + Once enabled, this callback is triggered at the set interval, regardless of whether a user speaks or not. + The SDK triggers two independent `onAudioVolumeIndication` callbacks at one time, which separately report the volume information of the local user and all the remote speakers. + For more information, see the detailed parameter descriptions. + @note + - To enable the voice activity detection of the local user, ensure that you set `report_vad`(true) in the `enableAudioVolumeIndication` method. + - Calling the \ref agora::rtc::IRtcEngine::muteLocalAudioStream "muteLocalAudioStream" method affects the SDK's behavior: + - If the local user calls the \ref agora::rtc::IRtcEngine::muteLocalAudioStream "muteLocalAudioStream" method, the SDK stops triggering the local user's callback. + - 20 seconds after a remote speaker calls the *muteLocalAudioStream* method, the remote speakers' callback excludes this remote user's information; 20 seconds after all remote users call the *muteLocalAudioStream* method, the SDK stops triggering the remote speakers' callback. + - An empty @p speakers array in the *onAudioVolumeIndication* callback suggests that no remote user is speaking at the moment. + @param speakers A pointer to AudioVolumeInfo: + - In the local user's callback, this struct contains the following members: + - `uid` = 0, + - `volume` = `totalVolume`, which reports the sum of the voice volume and audio-mixing volume of the local user, and + - `vad`, which reports the voice activity status of the local user. + - In the remote speakers' callback, this array contains the following members: + - `uid` of the remote speaker, + - `volume`, which reports the sum of the voice volume and audio-mixing volume of each remote speaker, and + - `vad` = 0. + An empty speakers array in the callback indicates that no remote user is speaking at the moment. + @param speakerNumber Total number of speakers. The value range is [0, 3]. + - In the local user's callback, `speakerNumber` = 1, regardless of whether the local user speaks or not. + - In the remote speakers' callback, the callback reports the IDs and volumes of the three loudest speakers when there are more than three remote users in the channel, and `speakerNumber` = 3. + @param totalVolume Total volume after audio mixing. The value ranges between 0 (lowest volume) and 255 (highest volume). + - In the local user's callback, `totalVolume` is the sum of the voice volume and audio-mixing volume of the local user. + - In the remote speakers' callback, `totalVolume` is the sum of the voice volume and audio-mixing volume of all the remote speakers. + */ + virtual void onAudioVolumeIndication(const AudioVolumeInfo* speakers, unsigned int speakerNumber, int totalVolume) { + if (m_hMsgHanlder) + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_AUDIO_VOLUME_INDICATION), totalVolume, 0); + } + + /** Reports the last mile network quality of the local user once every two seconds before the user joins the channel. + Last mile refers to the connection between the local device and Agora's edge server. After the application calls the \ref IRtcEngine::enableLastmileTest "enableLastmileTest" method, this callback reports once every two seconds the uplink and downlink last mile network conditions of the local user before the user joins the channel. + @param quality The last mile network quality: #QUALITY_TYPE. + */ + void onLastmileQuality(int quality) { + if (m_hMsgHanlder) + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LASTMILE_QUAILTY), quality, 0); + } + + /** Reports the last-mile network probe result. + The SDK triggers this callback within 30 seconds after the app calls the \ref agora::rtc::IRtcEngine::startLastmileProbeTest "startLastmileProbeTest" method. + @param result The uplink and downlink last-mile network probe test result. See LastmileProbeResult. + */ + void onLastmileProbeResult(LastmileProbeResult) { + if (m_hMsgHanlder) + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LASTMILE_PROBE_RESULT), 0,0); + } +private: + HWND m_hMsgHanlder; +}; + + +class CAgoraPreCallTestDlg : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraPreCallTestDlg) + +public: + CAgoraPreCallTestDlg(CWnd* pParent = nullptr); + virtual ~CAgoraPreCallTestDlg(); + + enum { IDD = IDD_DIALOG_PERCALL_TEST }; + + //Initialize the Ctrl Text. + void InitCtrlText(); + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //resume window status + void ResumeStatus(); + + void UpdateViews(); + + +private: + + IRtcEngine* m_rtcEngine; + CImageList m_imgNetQuality; + int m_netQuality; + CAGVideoTestWnd m_VideoTest; + CAgoraPreCallTestEvnetHandler m_eventHandler; + AAudioDeviceManager * m_audioDeviceManager; + AVideoDeviceManager * m_videoDeviceManager; + std::map m_mapAudioInput; + std::map m_mapAudioOutput; + std::map m_mapCamera; + bool m_audioInputTest; + bool m_audioOutputTest; + bool m_cameraTest; + +protected: + virtual void DoDataExchange(CDataExchange* pDX); + LRESULT afx_msg OnEIDLastmileQuality(WPARAM wparam,LPARAM lparam); + LRESULT afx_msg OnEIDLastmileProbeResult(WPARAM wparam, LPARAM lparam); + LRESULT afx_msg OnEIDAudioVolumeIndication(WPARAM wparam, LPARAM lparam); + + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + virtual BOOL OnInitDialog(); + virtual BOOL PreTranslateMessage(MSG* pMsg); + afx_msg void OnBnClickedButtonAudioInputTest(); + afx_msg void OnBnClickedButtonAudioOutputTest(); + afx_msg void OnBnClickedButtonCamera(); + afx_msg void OnSelchangeListInfoBroadcasting(); + afx_msg void OnReleasedcaptureSliderInputVol(NMHDR *pNMHDR, LRESULT *pResult); + afx_msg void OnReleasedcaptureSliderOutputVol(NMHDR *pNMHDR, LRESULT *pResult); + afx_msg void OnPaint(); + DECLARE_MESSAGE_MAP() +public: + CStatic m_staAudioInput; + CStatic m_staAudioInputVol; + CStatic m_staAudioOutput; + CStatic m_staAudioOutputVol; + CStatic m_staVideo; + CComboBox m_cmbVideo; + CComboBox m_cmbAudioInput; + CComboBox m_cmbAudioOutput; + CSliderCtrl m_sldAudioInputVol; + CSliderCtrl m_sldAudioOutputVol; + CButton m_btnAudioInputTest; + CButton m_btnAudioOutputTest; + CButton m_btnVideoTest; + CStatic m_staVideoArea; + CListBox m_lstInfo; + CStatic m_staDetails; +}; diff --git a/windows/APIExample/APIExample/Advanced/RTMPStream/AgoraRtmpStreaming.cpp b/windows/APIExample/APIExample/Advanced/RTMPStream/AgoraRtmpStreaming.cpp new file mode 100644 index 000000000..4e6607ad3 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/RTMPStream/AgoraRtmpStreaming.cpp @@ -0,0 +1,635 @@ +// AgoraRtmpStreaming.cpp : implementation file + + +#include "stdafx.h" +#include "APIExample.h" +#include "AgoraRtmpStreaming.h" + /* +note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one +parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). +*/ +void CAgoraRtmpStreamingDlgRtcEngineEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } +} + +/* +note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. +parameters: + stats: Call statistics. +*/ +void CAgoraRtmpStreamingDlgRtcEngineEventHandler::onLeaveChannel(const RtcStats& stats) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } +} + +/* +note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. +parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). +*/ +void CAgoraRtmpStreamingDlgRtcEngineEventHandler::onUserJoined(uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } +} +/* +note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. +parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. +*/ +void CAgoraRtmpStreamingDlgRtcEngineEventHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } +} +/** + Occurs when the state of the RTMP streaming changes. + The SDK triggers this callback to report the result of the local user calling the \ref agora::rtc::IRtcEngine::addPublishStreamUrl "addPublishStreamUrl" or \ref agora::rtc::IRtcEngine::removePublishStreamUrl "removePublishStreamUrl" method. + This callback indicates the state of the RTMP streaming. When exceptions occur, you can troubleshoot issues by referring to the detailed error descriptions in the *errCode* parameter. + @param url The RTMP URL address. + @param state The RTMP streaming state. See: #RTMP_STREAM_PUBLISH_STATE. + @param errCode The detailed error information for streaming. See: #RTMP_STREAM_PUBLISH_ERROR. + */ +void CAgoraRtmpStreamingDlgRtcEngineEventHandler::onRtmpStreamingStateChanged(const char *url, RTMP_STREAM_PUBLISH_STATE state, RTMP_STREAM_PUBLISH_ERROR errCode) +{ + if (m_hMsgHanlder) { + PRtmpStreamStreamStateChanged rtmpState = new RtmpStreamStreamStateChanged; + int len = strlen(url); + rtmpState->url = new char[len + 1]; + rtmpState->url[len] = 0; + strcpy_s(rtmpState->url, len + 1, url); + rtmpState->state = state; + rtmpState->error = errCode; + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_RTMP_STREAM_STATE_CHANGED), (WPARAM)rtmpState, 0); + } +} + + +IMPLEMENT_DYNAMIC(CAgoraRtmpStreamingDlg, CDialogEx) + +CAgoraRtmpStreamingDlg::CAgoraRtmpStreamingDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_RTMP_STREAMING, pParent) +{ + +} + +CAgoraRtmpStreamingDlg::~CAgoraRtmpStreamingDlg() +{ +} + +void CAgoraRtmpStreamingDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannelName); + DDX_Control(pDX, IDC_EDIT_RTMP_URL, m_edtRtmpUrl); + DDX_Control(pDX, IDC_BUTTON_ADDSTREAM, m_btnAddStream); + DDX_Control(pDX, IDC_BUTTON_REMOVE_STREAM, m_btnRemoveStream); + DDX_Control(pDX, IDC_COMBO_RTMPURLS, m_cmbRtmpUrl); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannelName); + DDX_Control(pDX, IDC_STATIC_RTMP_URL, m_staPublishUrl); + DDX_Control(pDX, IDC_BUTTON_REMOVE_ALLSTREAM, m_btnRemoveAll); + DDX_Control(pDX, IDC_STATIC_RTMP_URLS, m_staRemoveUrl); + DDX_Control(pDX, IDC_STATIC_DETAIL, m_staDetail); + DDX_Control(pDX, IDC_CHK_TRANS_CODING, m_chkTransCoding); + + DDX_Control(pDX, IDC_CHK_TRANS_CODING, m_chkTransCoding); +} + + +BEGIN_MESSAGE_MAP(CAgoraRtmpStreamingDlg, CDialogEx) + ON_WM_SHOWWINDOW() + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraRtmpStreamingDlg::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraRtmpStreamingDlg::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_RTMP_STREAM_STATE_CHANGED), &CAgoraRtmpStreamingDlg::OnEIDRtmpStateChanged) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraRtmpStreamingDlg::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraRtmpStreamingDlg::OnEIDUserOffline) + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraRtmpStreamingDlg::OnBnClickedButtonJoinchannel) + ON_BN_CLICKED(IDC_BUTTON_ADDSTREAM, &CAgoraRtmpStreamingDlg::OnBnClickedButtonAddstream) + ON_BN_CLICKED(IDC_BUTTON_REMOVE_STREAM, &CAgoraRtmpStreamingDlg::OnBnClickedButtonRemoveStream) + ON_BN_CLICKED(IDC_BUTTON_REMOVE_ALLSTREAM, &CAgoraRtmpStreamingDlg::OnBnClickedButtonRemoveAllstream) + ON_LBN_SELCHANGE(IDC_LIST_INFO_BROADCASTING, &CAgoraRtmpStreamingDlg::OnSelchangeListInfoBroadcasting) +END_MESSAGE_MAP() + + +//Initialize the Agora SDK +bool CAgoraRtmpStreamingDlg::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //enable video in the engine. + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + m_btnJoinChannel.EnableWindow(TRUE); + return true; +} +//UnInitialize the Agora SDK +void CAgoraRtmpStreamingDlg::UnInitAgora() +{ + if (m_rtcEngine) { + if (m_joinChannle) + m_rtcEngine->leaveChannel(); + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} +//render local video from SDK local capture. +void CAgoraRtmpStreamingDlg::RenderLocalVideo() +{ + if (m_rtcEngine) { + //start preview in the engine. + m_rtcEngine->startPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("startPreview")); + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_localVideoWnd.GetSafeHwnd(); + //setup local video in the engine to canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setupLocalVideo")); + + } +} + +BOOL CAgoraRtmpStreamingDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + m_localVideoWnd.MoveWindow(&rcArea); + m_localVideoWnd.ShowWindow(SW_SHOW); + ResumeStatus(); + return TRUE; +} + +//set control text from config. +void CAgoraRtmpStreamingDlg::InitCtrlText() +{ + m_chkTransCoding.SetWindowText(rtmpStreamingCtrlTransCoding); + m_staRemoveUrl.SetWindowText(rtmpStreamingCtrlPublishUrl); + m_staPublishUrl.SetWindowText(rtmpStreamingCtrlPublishUrl); + m_btnAddStream.SetWindowText(rtmpStreamingCtrlAdd); + m_staChannelName.SetWindowText(commonCtrlChannel); + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + m_btnRemoveStream.SetWindowText(rtmpStreamingCtrlRemove); + m_btnRemoveAll.SetWindowText(rtmpStreamingCtrlRemoveAll); +} + +void CAgoraRtmpStreamingDlg::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow) { + InitCtrlText(); + RenderLocalVideo(); + } + else { + ResumeStatus(); + } +} + +//remove all rtmp stream in the engine. +void CAgoraRtmpStreamingDlg::RemoveAllRtmpUrls() +{ + m_bRemoveAll = true; + CString strUrl; + //remove all publish stream in the engine. + for (int i = 0; i < m_cmbRtmpUrl.GetCount(); ++i) { + m_cmbRtmpUrl.GetLBText(i, strUrl); + std::string szUrl = cs2utf8(strUrl); + m_rtcEngine->removePublishStreamUrl(szUrl.c_str()); + } + m_cmbRtmpUrl.Clear(); + m_cmbRtmpUrl.ResetContent(); +} + +void CAgoraRtmpStreamingDlg::ResumeStatus() +{ + m_lstInfo.ResetContent(); + m_btnAddStream.EnableWindow(FALSE); + InitCtrlText(); + m_joinChannle = false; + m_initialize = false; + m_urlSet.clear(); + m_removeUrlCount = 0; + m_bRemoveAll = false; + m_edtRtmpUrl.SetWindowText(_T("")); + m_chkTransCoding.SetCheck(0); + m_edtChannelName.SetWindowText(_T("")); + m_staDetail.SetWindowText(_T("")); + +} + +//join or leave channel button handler. +void CAgoraRtmpStreamingDlg::OnBnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + + if (!m_joinChannle) { + CString strChannelName; + m_edtChannelName.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + + std::string szChannelId = cs2utf8(strChannelName); + //join channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + m_btnJoinChannel.EnableWindow(FALSE); + } + } + else { + if (m_cmbRtmpUrl.GetCount() > 0) { + m_btnAddStream.EnableWindow(FALSE); + m_btnRemoveStream.EnableWindow(FALSE); + m_btnJoinChannel.EnableWindow(FALSE); + //remove all rtmp streams. + RemoveAllRtmpUrls(); + } + //leave channel in the engine. + else if (0 == m_rtcEngine->leaveChannel()) { + m_btnJoinChannel.EnableWindow(FALSE); + } + } +} + +// add stream button handler. +void CAgoraRtmpStreamingDlg::OnBnClickedButtonAddstream() +{ + if (!m_rtcEngine || !m_initialize) + return; + + CString strURL; + m_edtRtmpUrl.GetWindowText(strURL); + if (strURL.IsEmpty()) { + AfxMessageBox(_T("Fill Publish URL first")); + return; + } + if (m_urlSet.find(strURL) != m_urlSet.end()) { + AfxMessageBox(_T("have add Publish Stream URL")); + return; + } + std::string szURL = cs2utf8(strURL); + BOOL isTransCoding = m_chkTransCoding.GetCheck(); + // add publish stream in the engine. + int ret = m_rtcEngine->addPublishStreamUrl(szURL.c_str(), isTransCoding); + + if (ret != 0) { + CString strInfo; + strInfo.Format(_T("addPublishStreamUrl failed:%d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return; + } +} + + +//remove stream button handler. +void CAgoraRtmpStreamingDlg::OnBnClickedButtonRemoveStream() +{ + if (!m_rtcEngine || !m_initialize) + return; + + if (m_cmbRtmpUrl.GetCount() == 0) + return; + + CString strUrl; + m_cmbRtmpUrl.GetWindowText(strUrl); + std::string szUrl = cs2utf8(strUrl); + //remove publish stream in the engine. + m_rtcEngine->removePublishStreamUrl(szUrl.c_str()); +} + +//remove all streams in the engine. +void CAgoraRtmpStreamingDlg::OnBnClickedButtonRemoveAllstream() +{ + if (m_cmbRtmpUrl.GetCount() == 0 && m_cmbRtmpUrl.GetCurSel() >= 0) { + return; + } + if (!m_rtcEngine || !m_initialize) + return; + + CString strUrl; + m_cmbRtmpUrl.GetWindowText(strUrl); + + std::string szUrl = cs2utf8(strUrl); + //remove public stream in the engine. + m_rtcEngine->removePublishStreamUrl(szUrl.c_str()); + m_btnRemoveStream.EnableWindow(FALSE); +} + +//EID_JOINCHANNEL_SUCCESS message window handler. +LRESULT CAgoraRtmpStreamingDlg::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_btnJoinChannel.EnableWindow(TRUE); + m_btnAddStream.EnableWindow(TRUE); + m_btnRemoveStream.EnableWindow(TRUE); + m_joinChannle = true; + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + + m_btnAddStream.EnableWindow(TRUE); + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); + return 0; +} + + +//Change liveTranscoding when users joined +LRESULT CAgoraRtmpStreamingDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + TranscodingUser tanrsCodingUser; + auto p = new TranscodingUser[++m_liveTransCoding.userCount]; + if (m_liveTransCoding.userCount != 1) + { + memcpy(p, m_liveTransCoding.transcodingUsers, sizeof(TranscodingUser)*m_liveTransCoding.userCount++); + free(m_liveTransCoding.transcodingUsers); + } + tanrsCodingUser.uid = wParam; + tanrsCodingUser.alpha = 1; + tanrsCodingUser.y = 0; + tanrsCodingUser.height = m_liveTransCoding.height; + tanrsCodingUser.width = m_liveTransCoding.width / m_liveTransCoding.userCount; + p[m_liveTransCoding.userCount - 1] = tanrsCodingUser; + for (size_t i = 0; i < m_liveTransCoding.userCount; i++) + { + p[i].x = tanrsCodingUser.width; + } + //add user info to TranscodingUsers. + m_liveTransCoding.transcodingUsers = p; + //set current live trans coding. + m_rtcEngine->setLiveTranscoding(m_liveTransCoding); + return TRUE; +} + + +//Change liveTranscoding when users leave +LRESULT CAgoraRtmpStreamingDlg::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + for (size_t i = 0; i < m_liveTransCoding.userCount; i++) + { + if (m_liveTransCoding.transcodingUsers[i].uid == wParam) + { + for (size_t j = i; j < m_liveTransCoding.userCount; j++) + { + m_liveTransCoding.transcodingUsers[j] = m_liveTransCoding.transcodingUsers[j + 1]; + } + } + } + m_liveTransCoding.userCount--; + int width = m_liveTransCoding.width / m_liveTransCoding.userCount; + for (size_t i = 0; i < m_liveTransCoding.userCount; i++) + { + m_liveTransCoding.transcodingUsers[i].x = width * i; + m_liveTransCoding.transcodingUsers[i].width = width; + } + //set current live trans coding. + m_rtcEngine->setLiveTranscoding(m_liveTransCoding); + return TRUE; +} + +//EID_LEAVE_CHANNEL message window handler. +LRESULT CAgoraRtmpStreamingDlg::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + m_btnJoinChannel.EnableWindow(TRUE); + m_joinChannle = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + CString strInfo; + strInfo.Format(_T("leave channel success")); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_btnRemoveStream.EnableWindow(FALSE); + m_btnAddStream.EnableWindow(TRUE); + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return 0; +} +//EID_INJECT_STATUS message window handler. +LRESULT CAgoraRtmpStreamingDlg::OnEIDRtmpStateChanged(WPARAM wParam, LPARAM lParam) +{ + PRtmpStreamStreamStateChanged rtmpState = (PRtmpStreamStreamStateChanged)wParam; + CString strInfo; + m_btnRemoveStream.EnableWindow(TRUE); + switch (rtmpState->state) + { + case RTMP_STREAM_PUBLISH_STATE_IDLE: + { + strInfo.Format(_T("%s:%S"), agoraRtmpStateIdle, rtmpState->url); + CString strUrl; + strUrl.Format(_T("%S"), rtmpState->url); + int sel = m_cmbRtmpUrl.GetCurSel(); + m_cmbRtmpUrl.DeleteString(sel); + m_cmbRtmpUrl.ResetContent(); + if (m_cmbRtmpUrl.GetCount() > 0) { + m_cmbRtmpUrl.SetCurSel(0); + } + for (auto iter = m_urlSet.begin(); iter != m_urlSet.end(); ++iter) + if (strUrl.Compare(*iter) == 0) { + m_urlSet.erase(iter); + break; + } + if (m_bRemoveAll) { + m_removeUrlCount++; + if (m_removeUrlCount == m_urlSet.size()) {//remove all url when leave channel + m_urlSet.clear(); + m_bRemoveAll = false; + m_rtcEngine->leaveChannel(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("leaveChannel")); + } + } + + } + break; + case RTMP_STREAM_PUBLISH_STATE_CONNECTING: + { + strInfo = agoraRtmpStateConnecting; + } + break; + case RTMP_STREAM_PUBLISH_STATE_RUNNING: + strInfo = agoraRtmpStateRunning; + if (rtmpState->error == RTMP_STREAM_PUBLISH_ERROR_OK) { + strInfo = agoraRtmpStateRunningSuccess; + CString strUrl; + strUrl.Format(_T("%S"), rtmpState->url); + if (m_urlSet.find(strUrl) == m_urlSet.end()) { + m_cmbRtmpUrl.AddString(strUrl); + m_urlSet.insert(strUrl); + if (m_cmbRtmpUrl.GetCurSel() < 0) + m_cmbRtmpUrl.SetCurSel(0); + } + + } + break; + case RTMP_STREAM_PUBLISH_STATE_RECOVERING: + strInfo.Format(agoraRtmpStateRecovering); + break; + case RTMP_STREAM_PUBLISH_STATE_FAILURE: + { + switch (rtmpState->state) + { + case RTMP_STREAM_PUBLISH_ERROR_INVALID_ARGUMENT: + { + strInfo = agoraRtmpStateInvalidArg; + } + break; + case RTMP_STREAM_PUBLISH_ERROR_ENCRYPTED_STREAM_NOT_ALLOWED: + { + strInfo = agoraRtmpStateEncrypted; + } + break; + case RTMP_STREAM_PUBLISH_ERROR_CONNECTION_TIMEOUT: + { + strInfo = agoraRtmpStateConnTimeout; + } + break; + case RTMP_STREAM_PUBLISH_ERROR_INTERNAL_SERVER_ERROR: + { + strInfo = agoraRtmpStateInrealErr; + } + break; + case RTMP_STREAM_PUBLISH_ERROR_RTMP_SERVER_ERROR: + { + strInfo = agoraRtmpStateServerErr; + } + break; + case RTMP_STREAM_PUBLISH_ERROR_TOO_OFTEN: + { + strInfo = agoraRtmpStateTooOften; + } + break; + case RTMP_STREAM_PUBLISH_ERROR_REACH_LIMIT: + { + strInfo = agoraRtmpStateReachLimit; + } + break; + case RTMP_STREAM_PUBLISH_ERROR_NOT_AUTHORIZED: + { + strInfo = agoraRtmpStateNotAuth; + } + break; + case RTMP_STREAM_PUBLISH_ERROR_STREAM_NOT_FOUND: + { + strInfo = agoraRtmpStateNotFound; + } + break; + case RTMP_STREAM_PUBLISH_ERROR_FORMAT_NOT_SUPPORTED: + { + strInfo = agoraRtmpStateNotSupported; + } + break; + default: + break; + } + } + break; + default: + break; + } + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + delete[] rtmpState->url; + rtmpState->url = NULL; + delete[] rtmpState; + rtmpState = NULL; + return 0; +} + +//show list information to m_staDetail. +void CAgoraRtmpStreamingDlg::OnSelchangeListInfoBroadcasting() +{ + int sel = m_lstInfo.GetCurSel(); + if (sel < 0)return; + CString strDetail; + m_lstInfo.GetText(sel, strDetail); + m_staDetail.SetWindowText(strDetail); +} + + + +BOOL CAgoraRtmpStreamingDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} + + + diff --git a/windows/APIExample/APIExample/Advanced/RTMPStream/AgoraRtmpStreaming.h b/windows/APIExample/APIExample/Advanced/RTMPStream/AgoraRtmpStreaming.h new file mode 100644 index 000000000..d9c2c7f54 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/RTMPStream/AgoraRtmpStreaming.h @@ -0,0 +1,154 @@ +#pragma once +#include "AGVideoWnd.h" +#include + +class CAgoraRtmpStreamingDlgRtcEngineEventHandler + : public IRtcEngineEventHandler +{ +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override; + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override; + /** + Occurs when the state of the RTMP streaming changes. + The SDK triggers this callback to report the result of the local user calling the \ref agora::rtc::IRtcEngine::addPublishStreamUrl "addPublishStreamUrl" or \ref agora::rtc::IRtcEngine::removePublishStreamUrl "removePublishStreamUrl" method. + This callback indicates the state of the RTMP streaming. When exceptions occur, you can troubleshoot issues by referring to the detailed error descriptions in the *errCode* parameter. + @param url The RTMP URL address. + @param state The RTMP streaming state. See: #RTMP_STREAM_PUBLISH_STATE. + @param errCode The detailed error information for streaming. See: #RTMP_STREAM_PUBLISH_ERROR. + */ + virtual void onRtmpStreamingStateChanged(const char *url, RTMP_STREAM_PUBLISH_STATE state, RTMP_STREAM_PUBLISH_ERROR errCode)override; + +private: + HWND m_hMsgHanlder; +}; + +// CAgoraRtmpStreamingDlg dialog + +class CAgoraRtmpStreamingDlg : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraRtmpStreamingDlg) + +public: + CAgoraRtmpStreamingDlg(CWnd* pParent = nullptr); // standard constructor + virtual ~CAgoraRtmpStreamingDlg(); + + // Dialog Data + enum { IDD = IDD_DIALOG_RTMP_STREAMING }; + +protected: + virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support + + DECLARE_MESSAGE_MAP() + +public: + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //set control text from config. + void InitCtrlText(); + //render local video from SDK local capture. + void RenderLocalVideo(); + //remove all rtmp stream in the engine. + void RemoveAllRtmpUrls(); + // resume window status. + void ResumeStatus(); + +private: + IRtcEngine* m_rtcEngine = nullptr; + CAgoraRtmpStreamingDlgRtcEngineEventHandler m_eventHandler; + CAGVideoWnd m_localVideoWnd; + bool m_joinChannle = false; + bool m_initialize = false; + bool m_bRemoveAll = false; + int m_removeUrlCount = 0; + std::set m_urlSet; + + LiveTranscoding m_liveTransCoding; +public: + virtual BOOL OnInitDialog(); + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + afx_msg void OnBnClickedButtonJoinchannel(); + afx_msg void OnBnClickedButtonAddstream(); + afx_msg void OnBnClickedButtonRemoveStream(); + afx_msg void OnBnClickedButtonRemoveAllstream(); + + afx_msg LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnEIDRtmpStateChanged(WPARAM wParam, LPARAM lParam); + afx_msg void OnSelchangeListInfoBroadcasting(); + virtual BOOL PreTranslateMessage(MSG* pMsg); + + CEdit m_edtChannelName; + CEdit m_edtRtmpUrl; + CButton m_btnAddStream; + CButton m_btnRemoveStream; + CComboBox m_cmbRtmpUrl; + CStatic m_staChannelName; + CStatic m_staPublishUrl; + CButton m_btnRemoveAll; + CStatic m_staRemoveUrl; + // m_staVideoArea + CButton m_btnJoinChannel; + CListBox m_lstInfo; + CStatic m_staVideoArea; + CStatic m_staDetail; + CButton m_chkTransCoding; +}; diff --git a/windows/APIExample/APIExample/Advanced/RTMPinject/AgoraRtmpInjectionDlg.cpp b/windows/APIExample/APIExample/Advanced/RTMPinject/AgoraRtmpInjectionDlg.cpp new file mode 100644 index 000000000..4fe59bb94 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/RTMPinject/AgoraRtmpInjectionDlg.cpp @@ -0,0 +1,471 @@ +// AgoraRtmpInjectionDlg.cpp : implementation file + + +#include "stdafx.h" +#include "APIExample.h" +#include "AgoraRtmpInjectionDlg.h" +/* +note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one +parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). +*/ +void CAgoraRtmpInjectionRtcEngineEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } +} +/* + Enter the online media stream status callback.This callback indicates the state + of the external video stream being input to the live stream. +parameters: + url:Enter the URL address of the external video source into the live stream + uid:user id. + status: + Input state of external video source: + INJECT_STREAM_STATUS_START_SUCCESS(0):External video stream input successful + INJECT_STREAM_STATUS_START_ALREADY_EXIST(1): External video stream already exists. + INJECT_STREAM_STATUS_START_UNAUTHORIZED(2): The external video stream input is unauthorized + INJECT_STREAM_STATUS_START_TIMEDOUT(3): Input external video stream timeout + INJECT_STREAM_STATUS_START_FAILED(4) : External video stream input failed + INJECT_STREAM_STATUS_STOP_SUCCESS(5) : INJECT_STREAM_STATUS_STOP_SUCCESS: External video stream stop input successful + INJECT_STREAM_STATUS_STOP_NOT_FOUND (6): No external video stream to stop input + INJECT_STREAM_STATUS_STOP_UNAUTHORIZED(7): The input to an external video stream is UNAUTHORIZED + INJECT_STREAM_STATUS_STOP_TIMEDOUT(8) : Stopped input external video stream timeout + INJECT_STREAM_STATUS_STOP_FAILED(9) : Failed to stop input external video stream + INJECT_STREAM_STATUS_BROKEN(10) : Input external video stream has been broken +*/ +void CAgoraRtmpInjectionRtcEngineEventHandler::onStreamInjectedStatus(const char* url, uid_t uid, int status) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_INJECT_STATUS), (WPARAM)uid, (LPARAM)status); + } +} +/* +note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. +parameters: + stats: Call statistics. +*/ +void CAgoraRtmpInjectionRtcEngineEventHandler::onLeaveChannel(const RtcStats& stats) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } +} + +/* +note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. +parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). +*/ +void CAgoraRtmpInjectionRtcEngineEventHandler::onUserJoined(uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } +} +/* +note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. +parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. +*/ +void CAgoraRtmpInjectionRtcEngineEventHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } +} + +// CAgoraRtmpInjectionDlg dialog + +IMPLEMENT_DYNAMIC(CAgoraRtmpInjectionDlg, CDialogEx) + +CAgoraRtmpInjectionDlg::CAgoraRtmpInjectionDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_RTMPINJECT, pParent) +{ + +} + +CAgoraRtmpInjectionDlg::~CAgoraRtmpInjectionDlg() +{ +} + +void CAgoraRtmpInjectionDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_BUTTON_ADDSTREAM, m_btnAddStream); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannelName); + DDX_Control(pDX, IDC_EDIT_INJECT_URL, m_edtInjectUrl); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannelName); + DDX_Control(pDX, IDC_STATIC_INJECT_URL, m_staInjectUrl); + DDX_Control(pDX, IDC_STATIC_DETAIL, m_staDetail); +} + + +BEGIN_MESSAGE_MAP(CAgoraRtmpInjectionDlg, CDialogEx) + ON_BN_CLICKED(IDC_BUTTON_ADDSTREAM, &CAgoraRtmpInjectionDlg::OnBnClickedButtonAddstream) + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraRtmpInjectionDlg::OnBnClickedButtonJoinchannel) + ON_WM_SHOWWINDOW() + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraRtmpInjectionDlg::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraRtmpInjectionDlg::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_INJECT_STATUS), &CAgoraRtmpInjectionDlg::OnEIDStreamInjectedStatus) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraRtmpInjectionDlg::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraRtmpInjectionDlg::OnEIDUserOffline) + + ON_LBN_SELCHANGE(IDC_LIST_INFO_BROADCASTING, &CAgoraRtmpInjectionDlg::OnSelchangeListInfoBroadcasting) +END_MESSAGE_MAP() + + +// CAgoraRtmpInjectionDlg message handlers +BOOL CAgoraRtmpInjectionDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + + // TODO: Add extra initialization here + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 200); + + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + m_localVideoWnd.MoveWindow(&rcArea); + m_localVideoWnd.ShowWindow(SW_SHOW); + ResumeStatus(); + return TRUE; +} +//set control text from config. +void CAgoraRtmpInjectionDlg::InitCtrlText() +{ + m_staInjectUrl.SetWindowText(rtmpInjectCtrlUrl); + m_btnAddStream.SetWindowText(rtmpInjectCtrlInject); + m_staChannelName.SetWindowText(commonCtrlChannel); + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); +} +//Initialize the Agora SDK +bool CAgoraRtmpInjectionDlg::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //enable video in the engine. + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + + m_btnJoinChannel.EnableWindow(TRUE); + return true; +} +//UnInitialize the Agora SDK +void CAgoraRtmpInjectionDlg::UnInitAgora() +{ + if (m_rtcEngine) { + if(m_joinChannel) + m_joinChannel = !m_rtcEngine->leaveChannel(); + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} +//render local video from SDK local capture. +void CAgoraRtmpInjectionDlg::RenderLocalVideo() +{ + if (m_rtcEngine) { + //start preview in the engine. + m_rtcEngine->startPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("startPreview")); + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_localVideoWnd.GetSafeHwnd(); + //setup local video in the engine to canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setupLocalVideo")); + } +} +// resume window status. +void CAgoraRtmpInjectionDlg::ResumeStatus() +{ + InitCtrlText(); + m_lstInfo.ResetContent(); + m_joinChannel = false; + m_initialize = false; + m_addInjectStream = false; + m_injectUrl=""; + m_btnAddStream.EnableWindow(FALSE); + m_edtInjectUrl.EnableWindow(FALSE); + m_edtInjectUrl.SetWindowText(_T("")); + m_edtChannelName.SetWindowText(_T("")); + m_staDetail.SetWindowText(_T("")); +} + +//bShow is true when the window is displayed +void CAgoraRtmpInjectionDlg::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow) { + RenderLocalVideo(); + } + else { + ResumeStatus(); + } +} + +//add or remove stream in the engine. +void CAgoraRtmpInjectionDlg::OnBnClickedButtonAddstream() +{ + if (!m_rtcEngine || !m_initialize) + return; + + if (m_addInjectStream) { + m_addInjectStream = false; + m_edtInjectUrl.EnableWindow(TRUE); + m_btnAddStream.SetWindowText(_T("Inject URL")); + //remove inject stream in the engine. + int ret = m_rtcEngine->removeInjectStreamUrl(m_injectUrl.c_str()); + } + else { + CString strURL; + m_edtInjectUrl.GetWindowText(strURL); + if (strURL.IsEmpty()) { + AfxMessageBox(_T("Fill INJECT URL first")); + return; + } + + std::string szURL = cs2utf8(strURL); + InjectStreamConfig config; + //add Inject stream url in the engine. + m_rtcEngine->addInjectStreamUrl(szURL.c_str(), config); + m_injectUrl = szURL; + m_addInjectStream = true; + m_edtInjectUrl.EnableWindow(FALSE); + m_btnAddStream.SetWindowText(_T("Remove URL")); + } + m_btnAddStream.EnableWindow(FALSE); + m_edtInjectUrl.EnableWindow(FALSE); +} + +//join or leave channel +void CAgoraRtmpInjectionDlg::OnBnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + + if (!m_joinChannel) { + CString strChannelName; + m_edtChannelName.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + + std::string szChannelId = cs2utf8(strChannelName); + //join channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + m_btnJoinChannel.EnableWindow(FALSE); + } + } + else { + //leave channel in the engine. + if (0 == m_rtcEngine->leaveChannel()) { + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("leave channel")); + m_btnJoinChannel.EnableWindow(FALSE); + } + } +} +//EID_JOINCHANNEL_SUCCESS message window handler. +LRESULT CAgoraRtmpInjectionDlg::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_btnJoinChannel.EnableWindow(TRUE); + m_joinChannel = true; + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + m_btnAddStream.EnableWindow(TRUE); + m_edtInjectUrl.EnableWindow(TRUE); + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); + return 0; +} +//EID_LEAVE_CHANNEL message window handler. +LRESULT CAgoraRtmpInjectionDlg::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + m_btnJoinChannel.EnableWindow(TRUE); + m_joinChannel = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + + CString strInfo; + strInfo.Format(_T("leave channel success")); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return 0; +} +//EID_USER_JOINED message window handler. +LRESULT CAgoraRtmpInjectionDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + if (remoteUid == 666) {//inject stream + CString strInfo; + strInfo.Format(_T("%u joined, 666 is inject stream"), remoteUid); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + //mute audio stream and video stream in the engine. + m_rtcEngine->muteRemoteAudioStream(666, true); + m_rtcEngine->muteRemoteVideoStream(666, true); + } + + return 0; +} +//EID_USER_OFFLINE message window handler. +LRESULT CAgoraRtmpInjectionDlg::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + if (remoteUid == 666) {//inject stream + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + //setup remote video in the engine to canvas. + m_rtcEngine->setupRemoteVideo(canvas); + CString strInfo; + strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + return 0; +} + + +//EID_INJECT_STATUS message window handler. +LRESULT CAgoraRtmpInjectionDlg::OnEIDStreamInjectedStatus(WPARAM wParam, LPARAM lParam) +{ + CString strInfo; + switch ((INJECT_STREAM_STATUS)lParam) + { + case INJECT_STREAM_STATUS_START_SUCCESS: + strInfo.Format(_T("%s, err: %d"), agoraInjectStartSucc, 0); + break; + case INJECT_STREAM_STATUS_START_ALREADY_EXISTS: + strInfo.Format(_T("%s, err: %d"), agoraInjectExist, 1); + break; + case INJECT_STREAM_STATUS_START_UNAUTHORIZED: + strInfo.Format(_T("%s, err: %d"), agoraInjectStartUnAuth, 2); + break; + case INJECT_STREAM_STATUS_START_TIMEDOUT: + strInfo.Format(_T("%s, err: %d"), agoraInjectStartTimeout, 3); + break; + + case INJECT_STREAM_STATUS_START_FAILED: + strInfo.Format(_T("%s, err: %d"), agoraInjectStartFailed, 4); + break; + case INJECT_STREAM_STATUS_STOP_SUCCESS: + strInfo.Format(_T("%s, err: %d"), agoraInjectStopSuccess, 5); + break; + case INJECT_STREAM_STATUS_STOP_NOT_FOUND: + strInfo.Format(_T("%s, err: %d"), agoraInjectNotFound, 6); + break; + case INJECT_STREAM_STATUS_STOP_UNAUTHORIZED: + strInfo.Format(_T("%s, err: %d"), agoraInjectStopUnAuth, 7); + break; + + case INJECT_STREAM_STATUS_STOP_TIMEDOUT: + strInfo.Format(_T("%s, err: %d"), agoraInjectStopTimeout, 8); + break; + case INJECT_STREAM_STATUS_STOP_FAILED: + strInfo.Format(_T("%s, err: %d"), agoraInjectStopFailed, 9); + break; + case INJECT_STREAM_STATUS_BROKEN: + strInfo.Format(_T("%s, err: %d"), agoraInjectBroken, 10); + break; + default: + break; + } + + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_btnAddStream.EnableWindow(TRUE); + m_edtInjectUrl.EnableWindow(TRUE); + return 0; +} + + +//select list information +void CAgoraRtmpInjectionDlg::OnSelchangeListInfoBroadcasting() +{ + int sel = m_lstInfo.GetCurSel(); + if (sel < 0)return; + CString strDetail; + m_lstInfo.GetText(sel, strDetail); + m_staDetail.SetWindowText(strDetail); +} + + +BOOL CAgoraRtmpInjectionDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} diff --git a/windows/APIExample/APIExample/Advanced/RTMPinject/AgoraRtmpInjectionDlg.h b/windows/APIExample/APIExample/Advanced/RTMPinject/AgoraRtmpInjectionDlg.h new file mode 100644 index 000000000..cc2b97fe5 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/RTMPinject/AgoraRtmpInjectionDlg.h @@ -0,0 +1,150 @@ +#pragma once +#include +#include "AGVideoWnd.h" + +class CAgoraRtmpInjectionRtcEngineEventHandler + : public IRtcEngineEventHandler +{ +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override; + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override; + /* + Enter the online media stream status callback.This callback indicates the state + of the external video stream being input to the live stream. + parameters: + url:Enter the URL address of the external video source into the live stream + uid:user id. + status: + Input state of external video source: + INJECT_STREAM_STATUS_START_SUCCESS(0):External video stream input successful + INJECT_STREAM_STATUS_START_ALREADY_EXIST(1): External video stream already exists. + INJECT_STREAM_STATUS_START_UNAUTHORIZED(2): The external video stream input is unauthorized + INJECT_STREAM_STATUS_START_TIMEDOUT(3): Input external video stream timeout + INJECT_STREAM_STATUS_START_FAILED(4) : External video stream input failed + INJECT_STREAM_STATUS_STOP_SUCCESS(5) : INJECT_STREAM_STATUS_STOP_SUCCESS: External video stream stop input successful + INJECT_STREAM_STATUS_STOP_NOT_FOUND (6): No external video stream to stop input + INJECT_STREAM_STATUS_STOP_UNAUTHORIZED(7): The input to an external video stream is UNAUTHORIZED + INJECT_STREAM_STATUS_STOP_TIMEDOUT(8) : Stopped input external video stream timeout + INJECT_STREAM_STATUS_STOP_FAILED(9) : Failed to stop input external video stream + INJECT_STREAM_STATUS_BROKEN(10) : Input external video stream has been broken + */ + virtual void onStreamInjectedStatus(const char* url, uid_t uid, int status) override; + +private: + HWND m_hMsgHanlder; +}; + +// CAgoraRtmpInjectionDlg dialog + +class CAgoraRtmpInjectionDlg : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraRtmpInjectionDlg) + +public: + CAgoraRtmpInjectionDlg(CWnd* pParent = nullptr); // standard constructor + virtual ~CAgoraRtmpInjectionDlg(); + + enum { IDD = IDD_DIALOG_RTMPINJECT }; +protected: + virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support + + DECLARE_MESSAGE_MAP() +public: + afx_msg void OnBnClickedButtonAddstream(); + afx_msg void OnBnClickedButtonJoinchannel(); + afx_msg LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnEIDStreamInjectedStatus(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + virtual BOOL OnInitDialog(); + +public: + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //set control text from config. + void InitCtrlText(); + //render local video from SDK local capture. + void RenderLocalVideo(); + // resume window status. + void ResumeStatus(); +private: + CAgoraRtmpInjectionRtcEngineEventHandler m_eventHandler; + CAGVideoWnd m_localVideoWnd; + IRtcEngine* m_rtcEngine = nullptr; + bool m_joinChannel = false; + bool m_initialize = false; + std::string m_injectUrl; + bool m_addInjectStream = false; + +public: + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + afx_msg void OnSelchangeListInfoBroadcasting(); + CListBox m_lstInfo; + CButton m_btnJoinChannel; + CButton m_btnAddStream; + CEdit m_edtChannelName; + CEdit m_edtInjectUrl; + CStatic m_staVideoArea; + CStatic m_staChannelName; + CStatic m_staInjectUrl; + CStatic m_staDetail; + virtual BOOL PreTranslateMessage(MSG* pMsg); +}; diff --git a/windows/APIExample/APIExample/Advanced/RegionConn/CAgoraRegionConnDlg.cpp b/windows/APIExample/APIExample/Advanced/RegionConn/CAgoraRegionConnDlg.cpp new file mode 100644 index 000000000..6870993d0 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/RegionConn/CAgoraRegionConnDlg.cpp @@ -0,0 +1,293 @@ +#include "stdafx.h" +#include "APIExample.h" +#include "CAgoraRegionConnDlg.h" + + + +IMPLEMENT_DYNAMIC(CAgoraRegionConnDlg, CDialogEx) + +CAgoraRegionConnDlg::CAgoraRegionConnDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_REGIONAL_CONNECTION, pParent) +{ + +} + +CAgoraRegionConnDlg::~CAgoraRegionConnDlg() +{ +} + +void CAgoraRegionConnDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannel); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannel); + DDX_Control(pDX, IDC_STATIC_AREA_CODE, m_staAreaCode); + DDX_Control(pDX, IDC_COMBO_AREA_CODE, m_cmbAreaCode); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); + DDX_Control(pDX, IDC_STATIC_DETAIL, m_staDetails); +} + +LRESULT CAgoraRegionConnDlg::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = true; + m_btnJoinChannel.EnableWindow(TRUE); + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_localVideoWnd.SetUID(wParam); + //notify parent window + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); + return TRUE; +} + +LRESULT CAgoraRegionConnDlg::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + + CString strInfo; + strInfo.Format(_T("leave channel success %s"), getCurrentTime()); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return TRUE; +} + +LRESULT CAgoraRegionConnDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + CString strInfo; + strInfo.Format(_T("%u joined"), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return TRUE; +} + +LRESULT CAgoraRegionConnDlg::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + m_rtcEngine->setupRemoteVideo(canvas); + CString strInfo; + strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + + return TRUE; +} + + +BEGIN_MESSAGE_MAP(CAgoraRegionConnDlg, CDialogEx) + ON_WM_SHOWWINDOW() + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraRegionConnDlg::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraRegionConnDlg::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraRegionConnDlg::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraRegionConnDlg::OnEIDUserOffline) + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraRegionConnDlg::OnBnClickedButtonJoinchannel) + ON_LBN_SELCHANGE(IDC_LIST_INFO_BROADCASTING, &CAgoraRegionConnDlg::OnSelchangeListInfoBroadcasting) +END_MESSAGE_MAP() + + +//Initialize the Ctrl Text. +void CAgoraRegionConnDlg::InitCtrlText() +{ + m_staChannel.SetWindowText(commonCtrlChannel); + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + m_staAreaCode.SetWindowText(RegionConnCtrlAreaCode); +} + + +//Initialize the Agora SDK +bool CAgoraRegionConnDlg::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + CString area_code; + m_cmbAreaCode.GetWindowText(area_code); + + //set area code + context.areaCode = m_mapAreaCode[area_code]; + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //enable video in the engine. + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + return true; +} + + +//UnInitialize the Agora SDK +void CAgoraRegionConnDlg::UnInitAgora() +{ + if (m_rtcEngine) { + if (m_joinChannel) + //leave channel + m_joinChannel = !m_rtcEngine->leaveChannel(); + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} + +//render local video from SDK local capture. +void CAgoraRegionConnDlg::RenderLocalVideo() +{ + if (m_rtcEngine) { + //start preview in the engine. + m_rtcEngine->startPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("startPreview")); + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_localVideoWnd.GetSafeHwnd(); + //setup local video in the engine to canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setupLocalVideo")); + } +} + + +//resume window status +void CAgoraRegionConnDlg::ResumeStatus() +{ + InitCtrlText(); + m_lstInfo.ResetContent(); + m_edtChannel.SetWindowText(_T("")); + m_cmbAreaCode.SetCurSel(0); + m_joinChannel = false; + m_initialize = false; +} + + +void CAgoraRegionConnDlg::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow) { + InitCtrlText(); + RenderLocalVideo(); + } + else { + ResumeStatus(); + } +} + + +BOOL CAgoraRegionConnDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} + + +BOOL CAgoraRegionConnDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + m_localVideoWnd.MoveWindow(&rcArea); + m_localVideoWnd.ShowWindow(SW_SHOW); + + int nIndex = 0; + + m_cmbAreaCode.InsertString(nIndex++, _T("AREA_CODE_GLOB")); + m_cmbAreaCode.InsertString(nIndex++, _T("AREA_CODE_CN")); + m_cmbAreaCode.InsertString(nIndex++, _T("AREA_CODE_NA")); + m_cmbAreaCode.InsertString(nIndex++, _T("AREA_CODE_EU")); + m_cmbAreaCode.InsertString(nIndex++, _T("AREA_CODE_AS")); + m_cmbAreaCode.InsertString(nIndex++, _T("AREA_CODE_JP")); + m_cmbAreaCode.InsertString(nIndex++, _T("AREA_CODE_IN")); + + m_mapAreaCode.insert(std::make_pair(_T("AREA_CODE_CN"),AREA_CODE_CN)); + m_mapAreaCode.insert(std::make_pair(_T("AREA_CODE_NA"), AREA_CODE_NA)); + m_mapAreaCode.insert(std::make_pair(_T("AREA_CODE_EU"), AREA_CODE_EU)); + m_mapAreaCode.insert(std::make_pair(_T("AREA_CODE_AS"), AREA_CODE_AS)); + m_mapAreaCode.insert(std::make_pair(_T("AREA_CODE_JP"), AREA_CODE_JP)); + m_mapAreaCode.insert(std::make_pair(_T("AREA_CODE_IN"), AREA_CODE_IN)); + m_mapAreaCode.insert(std::make_pair(_T("AREA_CODE_GLOB"), AREA_CODE_GLOB)); + + m_cmbAreaCode.SetCurSel(0); + ResumeStatus(); + return TRUE; +} + + +void CAgoraRegionConnDlg::OnBnClickedButtonJoinchannel() +{ + if (!m_initialize) + { + InitAgora(); + RenderLocalVideo(); + } + if (!m_rtcEngine || !m_initialize) + return; + CString strInfo; + if (!m_joinChannel) { + CString strChannelName; + m_edtChannel.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + std::string szChannelId = cs2utf8(strChannelName); + //join channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + strInfo.Format(_T("join channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + } + } + else { + //leave channel in the engine. + if (0 == m_rtcEngine->leaveChannel()) { + strInfo.Format(_T("leave channel %s"), getCurrentTime()); + } + } + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + + +void CAgoraRegionConnDlg::OnSelchangeListInfoBroadcasting() +{ + int sel = m_lstInfo.GetCurSel(); + if (sel < 0)return; + CString strDetail; + m_lstInfo.GetText(sel, strDetail); + m_staDetails.SetWindowText(strDetail); +} diff --git a/windows/APIExample/APIExample/Advanced/RegionConn/CAgoraRegionConnDlg.h b/windows/APIExample/APIExample/Advanced/RegionConn/CAgoraRegionConnDlg.h new file mode 100644 index 000000000..25c486710 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/RegionConn/CAgoraRegionConnDlg.h @@ -0,0 +1,141 @@ +#pragma once +#include "AGVideoWnd.h" + + +class CAgoraRegionConnHandler : public IRtcEngineEventHandler +{ +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) + { + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } + } + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override + { + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } + } + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override + { + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } + } + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override + { + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } + } +private: + HWND m_hMsgHanlder; +}; + + + +class CAgoraRegionConnDlg : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraRegionConnDlg) + +public: + CAgoraRegionConnDlg(CWnd* pParent = nullptr); + virtual ~CAgoraRegionConnDlg(); + + enum { IDD = IDD_DIALOG_REGIONAL_CONNECTION }; + //Initialize the Ctrl Text. + void InitCtrlText(); + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //render local video from SDK local capture. + void RenderLocalVideo(); + //resume window status + void ResumeStatus(); + +private: + bool m_joinChannel = false; + bool m_initialize = false; + IRtcEngine* m_rtcEngine = nullptr; + CAGVideoWnd m_localVideoWnd; + CAgoraRegionConnHandler m_eventHandler; + std::map m_mapAreaCode; +protected: + virtual void DoDataExchange(CDataExchange* pDX); + // agora sdk message window handler + LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + DECLARE_MESSAGE_MAP() +public: + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + virtual BOOL PreTranslateMessage(MSG* pMsg); + virtual BOOL OnInitDialog(); + CStatic m_staVideoArea; + CStatic m_staChannel; + CEdit m_edtChannel; + CStatic m_staAreaCode; + CComboBox m_cmbAreaCode; + CButton m_btnJoinChannel; + CListBox m_lstInfo; + CStatic m_staDetails; + afx_msg void OnBnClickedButtonJoinchannel(); + afx_msg void OnSelchangeListInfoBroadcasting(); +}; diff --git a/windows/APIExample/APIExample/Advanced/ReportInCall/CAgoraReportInCallDlg.cpp b/windows/APIExample/APIExample/Advanced/ReportInCall/CAgoraReportInCallDlg.cpp new file mode 100644 index 000000000..12d8d32cc --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/ReportInCall/CAgoraReportInCallDlg.cpp @@ -0,0 +1,414 @@ +#include "stdafx.h" +#include "APIExample.h" +#include "CAgoraReportInCallDlg.h" + + +IMPLEMENT_DYNAMIC(CAgoraReportInCallDlg, CDialogEx) + +CAgoraReportInCallDlg::CAgoraReportInCallDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_PEPORT_IN_CALL, pParent) +{ + +} + +CAgoraReportInCallDlg::~CAgoraReportInCallDlg() +{ +} + +void CAgoraReportInCallDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannel); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannel); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_STATIC_NETWORK_TOTAL, m_gopNetWorkTotal); + DDX_Control(pDX, IDC_STATIC_AUDIO_REMOTE, m_gopAudioRemote); + DDX_Control(pDX, IDC_STATIC_VIDEO_REMOTE, m_gopVideoRemote); + DDX_Control(pDX, IDC_STATIC_TXBYTES_RXBTYES, m_staTotalBytes); + DDX_Control(pDX, IDC_STATIC_TXBYTES_RXBYTES_VAL, m_staTotalBytesVal); + DDX_Control(pDX, IDC_STATIC_BITRATE_ALL, m_staTotalBitrate); + DDX_Control(pDX, IDC_STATIC_BITRATE_ALL_VAL, m_staTotalBitrateVal); + DDX_Control(pDX, IDC_STATIC_AUDIO_NETWORK_DELAY, m_staAudioNetWorkDelay); + DDX_Control(pDX, IDC_STATIC_AUDIO_NETWORK_DELAY_VAL, m_staAudioNetWorkDelayVal); + DDX_Control(pDX, IDC_STATIC_AUDIO_RECIVED_BITRATE, m_staAudioRecvBitrate); + DDX_Control(pDX, IDC_STATIC_AUDIO_RECVIED_BITRATE_VAL, m_staAudioRecvBitrateVal); + DDX_Control(pDX, IDC_STATIC_VIDEO_NETWORK_DELAY, m_staVideoNetWorkDelay); + DDX_Control(pDX, IDC_STATIC_VEDIO_NETWORK_DELAY_VAL, m_staVideoNetWorkDelayVal); + DDX_Control(pDX, IDC_STATIC_VEDIO_RECIVED_BITRATE, m_staVideoRecvBitrate); + DDX_Control(pDX, IDC_STATIC_VEDIO_RECVIED_BITRATE_VAL2, m_staVideoRecvBitrateVal); + DDX_Control(pDX, IDC_STATIC_LOCAL_VIDEO_WIDTH_HEIGHT, m_staLocalVideoResoultion); + DDX_Control(pDX, IDC_STATIC_LOCAL_VIDEO_WITH_HEIGHT_VAL, m_staLocalVideoResoultionVal); + DDX_Control(pDX, IDC_STATIC_LOCAL_VIDEO_FPS, m_staLocalVideoFPS); + DDX_Control(pDX, IDC_STATIC_LOCAL_VIDEO_FPS_VAL, m_staLocalVideoFPSVal); + DDX_Control(pDX, IDC_STATIC_DETAIL, m_staDetails); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); +} + + +BEGIN_MESSAGE_MAP(CAgoraReportInCallDlg, CDialogEx) + ON_WM_SHOWWINDOW() + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraReportInCallDlg::OnBnClickedButtonJoinchannel) + ON_LBN_SELCHANGE(IDC_LIST_INFO_BROADCASTING, &CAgoraReportInCallDlg::OnSelchangeListInfoBroadcasting) + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraReportInCallDlg::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraReportInCallDlg::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraReportInCallDlg::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraReportInCallDlg::OnEIDUserOffline) + ON_MESSAGE(WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), &CAgoraReportInCallDlg::OnEIDRemoteVideoStateChanged) + + ON_MESSAGE(WM_MSGID(EID_RTC_STATS), &CAgoraReportInCallDlg::OnEIDRtcStats) + ON_MESSAGE(WM_MSGID(EID_REMOTE_VIDEO_STATS), &CAgoraReportInCallDlg::OnEIDRemoteVideoStats) + ON_MESSAGE(WM_MSGID(EID_REMOTE_AUDIO_STATS), &CAgoraReportInCallDlg::OnEIDRemoteAudioStats) + ON_MESSAGE(WM_MSGID(EID_LOCAL_VIDEO_STATS), &CAgoraReportInCallDlg::OnEIDLocalVideoStats) + +END_MESSAGE_MAP() + +//Initialize the Ctrl Text. +void CAgoraReportInCallDlg::InitCtrlText() +{ + m_staChannel.SetWindowText(commonCtrlChannel); + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + m_staLocalVideoFPS.SetWindowText(ReportInCallCtrlLocalFPS); + m_staLocalVideoResoultion.SetWindowText(ReportInCallCtrlLocalResoultion); + m_staTotalBitrate.SetWindowText(ReportInCallCtrlTotalBitrate); + m_staTotalBytes.SetWindowText(ReportInCallCtrlTotalBytes); + m_gopAudioRemote.SetWindowText(ReportInCallCtrlGopRemoteAudio); + m_gopVideoRemote.SetWindowText(ReportInCallCtrlGopRemoteVideo); + m_gopNetWorkTotal.SetWindowText(ReportInCallCtrlGopTotal); + m_staVideoRecvBitrate.SetWindowText(ReportInCallCtrlVideoBitrate); + m_staVideoNetWorkDelay.SetWindowText(ReportInCallCtrlVideoNetWorkDelay); + m_staAudioNetWorkDelay.SetWindowText(ReportInCallCtrlAudioNetWorkDelay); + m_staAudioRecvBitrate.SetWindowText(ReportInCallCtrlAudioBitrate); +} + +//Initialize the Agora SDK +bool CAgoraReportInCallDlg::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //enable video in the engine. + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + return true; +} + + +//UnInitialize the Agora SDK +void CAgoraReportInCallDlg::UnInitAgora() +{ + if (m_rtcEngine) { + if (m_joinChannel) + //leave channel + m_joinChannel = !m_rtcEngine->leaveChannel(); + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} + +//render local video from SDK local capture. +void CAgoraReportInCallDlg::RenderLocalVideo() +{ + if (m_rtcEngine) { + //start preview in the engine. + m_rtcEngine->startPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("startPreview")); + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_localVideoWnd.GetSafeHwnd(); + //setup local video in the engine to canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setupLocalVideo")); + } +} + + +//resume window status +void CAgoraReportInCallDlg::ResumeStatus() +{ + InitCtrlText(); + m_edtChannel.SetWindowText(_T("")); + m_lstInfo.ResetContent(); + m_staDetails.SetWindowText(_T("")); + + m_staLocalVideoFPSVal.SetWindowText(_T("")); + m_staLocalVideoResoultionVal.SetWindowText(_T("")); + m_staVideoRecvBitrateVal.SetWindowText(_T("")); + m_staAudioRecvBitrateVal.SetWindowText(_T("")); + m_staTotalBitrateVal.SetWindowText(_T("")); + m_staTotalBytesVal.SetWindowText(_T("")); + + m_staAudioNetWorkDelayVal.SetWindowText(_T("")); + m_staVideoNetWorkDelayVal.SetWindowText(_T("")); + + m_joinChannel = false; + m_initialize = false; + m_setEncrypt = false; +} + + + +void CAgoraReportInCallDlg::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow)//bShwo is true ,show window + { + InitCtrlText(); + RenderLocalVideo(); + } + else { + ResumeStatus(); + } +} + + +BOOL CAgoraReportInCallDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + m_localVideoWnd.MoveWindow(&rcArea); + m_localVideoWnd.ShowWindow(SW_SHOW); + ResumeStatus(); + return TRUE; +} + + +BOOL CAgoraReportInCallDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} + + +void CAgoraReportInCallDlg::OnBnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + CString strInfo; + if (!m_joinChannel) { + CString strChannelName; + m_edtChannel.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + std::string szChannelId = cs2utf8(strChannelName); + //join channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + strInfo.Format(_T("join channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + } + } + else { + //leave channel in the engine. + if (0 == m_rtcEngine->leaveChannel()) { + strInfo.Format(_T("leave channel %s"), getCurrentTime()); + } + } + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + + +// select change for list control handler +void CAgoraReportInCallDlg::OnSelchangeListInfoBroadcasting() +{ + int sel = m_lstInfo.GetCurSel(); + if (sel < 0)return; + CString strDetail; + m_lstInfo.GetText(sel, strDetail); + m_staDetails.SetWindowText(strDetail); +} + + +//EID_JOINCHANNEL_SUCCESS message window handler. +LRESULT CAgoraReportInCallDlg::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = true; + m_btnJoinChannel.EnableWindow(TRUE); + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_localVideoWnd.SetUID(wParam); + //notify parent window + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); + return 0; +} + +//EID_LEAVE_CHANNEL message window handler. +LRESULT CAgoraReportInCallDlg::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + + m_joinChannel = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + + CString strInfo; + strInfo.Format(_T("leave channel success %s"), getCurrentTime()); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return 0; +} + +//EID_USER_JOINED message window handler. +LRESULT CAgoraReportInCallDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + CString strInfo; + strInfo.Format(_T("%u joined"), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + + +//EID_USER_OFFLINE message window handler. +LRESULT CAgoraReportInCallDlg::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + m_rtcEngine->setupRemoteVideo(canvas); + CString strInfo; + strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + + return 0; +} + +//EID_REMOTE_VIDEO_STATE_CHANED message window handler. +LRESULT CAgoraReportInCallDlg::OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam) +{ + PVideoStateStateChanged stateChanged = (PVideoStateStateChanged)wParam; + if (stateChanged) { + //onRemoteVideoStateChanged + CString strSateInfo; + switch (stateChanged->state) { + case REMOTE_VIDEO_STATE_STARTING: + strSateInfo = _T("REMOTE_VIDEO_STATE_STARTING"); + break; + case REMOTE_VIDEO_STATE_STOPPED: + strSateInfo = _T("strSateInfo"); + break; + case REMOTE_VIDEO_STATE_DECODING: + strSateInfo = _T("REMOTE_VIDEO_STATE_DECODING"); + break; + case REMOTE_VIDEO_STATE_FAILED: + strSateInfo = _T("REMOTE_VIDEO_STATE_FAILED "); + break; + case REMOTE_VIDEO_STATE_FROZEN: + strSateInfo = _T("REMOTE_VIDEO_STATE_FROZEN "); + break; + } + CString strInfo; + strInfo.Format(_T("onRemoteVideoStateChanged: uid=%u, %s"), stateChanged->uid, strSateInfo); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + return 0; +} + +//refresh remote video stats +LRESULT CAgoraReportInCallDlg::OnEIDRemoteVideoStats(WPARAM wParam, LPARAM lParam) +{ + RemoteVideoStats * p = reinterpret_cast(wParam); + if (p) + { + CString tmp; + tmp.Format(_T("%dms"), p->delay); + m_staVideoNetWorkDelayVal.SetWindowText(tmp); + tmp.Format(_T("%dKbps"), p->receivedBitrate); + m_staVideoRecvBitrateVal.SetWindowText(tmp); + + delete p; + } + return TRUE; +} + +//refresh remote audio stats +LRESULT CAgoraReportInCallDlg::OnEIDRemoteAudioStats(WPARAM wParam, LPARAM lParam) +{ + RemoteAudioStats *p = reinterpret_cast(wParam); + if (p) + { + CString tmp; + tmp.Format(_T("%dms"), p->networkTransportDelay); + m_staAudioNetWorkDelayVal.SetWindowText(tmp); + + tmp.Format(_T("%dKbps"), p->receivedBitrate); + m_staAudioRecvBitrateVal.SetWindowText(tmp); + + delete p; + } + return TRUE; +} + +//refresh total bitrate and total bytes. +LRESULT CAgoraReportInCallDlg::OnEIDRtcStats(WPARAM wParam, LPARAM lParam) +{ + RtcStats *p = reinterpret_cast(wParam); + if (p) + { + CString tmp; + tmp.Format(_T("%dKbps/%dKbps"), p->txKBitRate, p->rxKBitRate); + m_staTotalBitrateVal.SetWindowText(tmp); + tmp.Format(_T("%.2fMB/%.2fMB"), p->txBytes ? p->txBytes / 1024.0 / 1024 : 0, p->rxBytes ? p->rxBytes / 1024.0 / 1024 : 0); + m_staTotalBytesVal.SetWindowText(tmp); + delete p; + } + return TRUE; +} + +//refresh local video stats +LRESULT CAgoraReportInCallDlg::OnEIDLocalVideoStats(WPARAM wParam, LPARAM lParam) +{ + LocalVideoStats *p = reinterpret_cast(wParam); + if (p) + { + CString tmp; + tmp.Format(_T("%d fps"), p->sentFrameRate); + m_staLocalVideoFPSVal.SetWindowText(tmp); + tmp.Format(_T("%d X %d"), p->encodedFrameWidth, p->encodedFrameHeight); + m_staLocalVideoResoultionVal.SetWindowText(tmp); + delete p; + } + return TRUE; +} diff --git a/windows/APIExample/APIExample/Advanced/ReportInCall/CAgoraReportInCallDlg.h b/windows/APIExample/APIExample/Advanced/ReportInCall/CAgoraReportInCallDlg.h new file mode 100644 index 000000000..d77501c74 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/ReportInCall/CAgoraReportInCallDlg.h @@ -0,0 +1,333 @@ +#pragma once +#include "AGVideoWnd.h" +#include + + +class CAgoraReportInCallHandler : public IRtcEngineEventHandler +{ +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override + { + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } + } + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override + { + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } + } + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override + { + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } + } + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override + { + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } + + } + /** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. + */ + virtual void onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) override + { + if (m_hMsgHanlder) { + PVideoStateStateChanged stateChanged = new VideoStateStateChanged; + stateChanged->uid = uid; + stateChanged->reason = reason; + stateChanged->state = state; + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), (WPARAM)stateChanged, 0); + } + } + + /** + Reports the last mile network quality of each user in the channel once every two seconds. + Last mile refers to the connection between the local device and Agora's edge server. This callback reports + once every two seconds the last mile network conditions of each user in the channel. If a channel includes + multiple users, the SDK triggers this callback as many times. + @param uid User ID. The network quality of the user with this @p uid is reported. If @p uid is 0, + the local network quality is reported. + @param txQuality Uplink transmission quality rating of the user in terms of the transmission bitrate, + packet loss rate, average RTT (Round-Trip Time), and jitter of the uplink network. + @p txQuality is a quality rating helping you understand how well the current uplink network conditions + can support the selected VideoEncoderConfiguration. For example, a 1000 Kbps uplink network may be adequate + for video frames with a resolution of 640 * 480 and a frame rate of 15 fps in the `LIVE_BROADCASTING` profile, + but may be inadequate for resolutions higher than 1280 * 720. See #QUALITY_TYPE. + @param rxQuality Downlink network quality rating of the user in terms of the packet loss rate, average RTT, + and jitter of the downlink network. See #QUALITY_TYPE. + */ + virtual void onNetworkQuality(uid_t uid, int txQuality, int rxQuality)override { + ; + } + + /** + Reports the statistics of the current call. + The SDK triggers this callback once every two seconds after the user joins the channel. + @param stats Statistics of the IRtcEngine: RtcStats. + */ + virtual void onRtcStats(const RtcStats& stats) { + if (m_hMsgHanlder) + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_RTC_STATS),(WPARAM)new RtcStats(stats), 0); + } + + /** + Reports the statistics of the local audio stream. + The SDK triggers this callback once every two seconds. + @param stats The statistics of the local audio stream. + See LocalAudioStats. + */ + virtual void onLocalAudioStats(const LocalAudioStats& stats) { + (void)stats; + } + + /** Occurs when the local audio state changes. + * This callback indicates the state change of the local audio stream, + * including the state of the audio recording and encoding, and allows + * you to troubleshoot issues when exceptions occur. + * + * @note + * When the state is #LOCAL_AUDIO_STREAM_STATE_FAILED (3), see the `error` + * parameter for details. + * + * @param state State of the local audio. See #LOCAL_AUDIO_STREAM_STATE. + * @param error The error information of the local audio. + * See #LOCAL_AUDIO_STREAM_ERROR. + */ + virtual void onLocalAudioStateChanged(LOCAL_AUDIO_STREAM_STATE state, LOCAL_AUDIO_STREAM_ERROR error) { + (void)state; + (void)error; + } + + /** + Reports the statistics of the audio stream from each remote user/host. + This callback replaces the \ref agora::rtc::IRtcEngineEventHandler::onAudioQuality "onAudioQuality" callback. + The SDK triggers this callback once every two seconds for each remote user/host. If a channel includes multiple remote users, the SDK triggers this callback as many times. + @param stats Pointer to the statistics of the received remote audio streams. See RemoteAudioStats. + */ + virtual void onRemoteAudioStats(const RemoteAudioStats& stats) { + if (m_hMsgHanlder) + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_REMOTE_AUDIO_STATS), (WPARAM)new RemoteAudioStats(stats), 0); + } + + + /** Occurs when the remote audio state changes. + + This callback indicates the state change of the remote audio stream. + @note This callback does not work properly when the number of users (in the `COMMUNICATION` profile) or hosts (in the `LIVE_BROADCASTING` profile) in the channel exceeds 17. + + @param uid ID of the remote user whose audio state changes. + @param state State of the remote audio. See #REMOTE_AUDIO_STATE. + @param reason The reason of the remote audio state change. + See #REMOTE_AUDIO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref IRtcEngine::joinChannel "joinChannel" method until the SDK + triggers this callback. + */ + virtual void onRemoteAudioStateChanged(uid_t uid, REMOTE_AUDIO_STATE state, REMOTE_AUDIO_STATE_REASON reason, int elapsed) { + (void)uid; + (void)state; + (void)reason; + (void)elapsed; + } + + /** Reports the statistics of the local video stream. + * + * The SDK triggers this callback once every two seconds for each + * user/host. If there are multiple users/hosts in the channel, the SDK + * triggers this callback as many times. + * + * @note + * If you have called the + * \ref agora::rtc::IRtcEngine::enableDualStreamMode "enableDualStreamMode" + * method, the \ref onLocalVideoStats() "onLocalVideoStats" callback + * reports the statistics of the high-video + * stream (high bitrate, and high-resolution video stream). + * + * @param stats Statistics of the local video stream. See LocalVideoStats. + */ + virtual void onLocalVideoStats(const LocalVideoStats& stats) { + if (m_hMsgHanlder) + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LOCAL_VIDEO_STATS), (WPARAM)new LocalVideoStats(stats), 0); + } + + /** Occurs when the local video stream state changes. + This callback indicates the state of the local video stream, including camera capturing and video encoding, and allows you to troubleshoot issues when exceptions occur. + @note For some device models, the SDK will not trigger this callback when the state of the local video changes while the local video capturing device is in use, so you have to make your own timeout judgment. + @param localVideoState State type #LOCAL_VIDEO_STREAM_STATE. When the state is LOCAL_VIDEO_STREAM_STATE_FAILED (3), see the `error` parameter for details. + @param error The detailed error information: #LOCAL_VIDEO_STREAM_ERROR. + */ + virtual void onLocalVideoStateChanged(LOCAL_VIDEO_STREAM_STATE localVideoState, LOCAL_VIDEO_STREAM_ERROR error) { + (void)localVideoState; + (void)error; + } + + /** Reports the statistics of the video stream from each remote user/host. + * + * The SDK triggers this callback once every two seconds for each remote + * user/host. If a channel includes multiple remote users, the SDK + * triggers this callback as many times. + * + * @param stats Statistics of the remote video stream. See + * RemoteVideoStats. + */ + virtual void onRemoteVideoStats(const RemoteVideoStats& stats) { + if (m_hMsgHanlder) + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_REMOTE_VIDEO_STATS), (WPARAM)new RemoteVideoStats(stats), 0); + } + +private: + HWND m_hMsgHanlder; +}; + + +class CAgoraReportInCallDlg : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraReportInCallDlg) + +public: + CAgoraReportInCallDlg(CWnd* pParent = nullptr); + virtual ~CAgoraReportInCallDlg(); + + enum { IDD = IDD_DIALOG_PEPORT_IN_CALL }; +public: + //Initialize the Ctrl Text. + void InitCtrlText(); + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //render local video from SDK local capture. + void RenderLocalVideo(); + //resume window status + void ResumeStatus(); + +private: + bool m_joinChannel = false; + bool m_initialize = false; + bool m_setEncrypt = false; + IRtcEngine* m_rtcEngine = nullptr; + CAGVideoWnd m_localVideoWnd; + CAgoraReportInCallHandler m_eventHandler; + + RemoteVideoStats m_remoteVideStats; + RemoteAudioStats m_remoteAudioStats; + + + +protected: + virtual void DoDataExchange(CDataExchange* pDX); + DECLARE_MESSAGE_MAP() + // agora sdk message window handler + LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDRemoteVideoStats(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDRemoteAudioStats(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDRtcStats(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDLocalVideoStats(WPARAM wParam, LPARAM lParam); + + + +public: + CStatic m_staChannel; + CEdit m_edtChannel; + CButton m_btnJoinChannel; + CStatic m_staVideoArea; + CStatic m_gopNetWorkTotal; + CStatic m_gopAudioRemote; + CStatic m_gopVideoRemote; + CStatic m_staUpDownLinkVal; + CStatic m_staTotalBytes; + CStatic m_staTotalBytesVal; + CStatic m_staTotalBitrate; + CStatic m_staTotalBitrateVal; + CStatic m_staAudioNetWorkDelay; + CStatic m_staAudioNetWorkDelayVal; + CStatic m_staAudioRecvBitrate; + CStatic m_staAudioRecvBitrateVal; + CStatic m_staVideoNetWorkDelay; + CStatic m_staVideoNetWorkDelayVal; + CStatic m_staVideoRecvBitrate; + CStatic m_staVideoRecvBitrateVal; + CStatic m_staLocalVideoResoultion; + CStatic m_staLocalVideoResoultionVal; + CStatic m_staLocalVideoFPS; + CStatic m_staLocalVideoFPSVal; + CStatic m_staDetails; + CListBox m_lstInfo; + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + virtual BOOL OnInitDialog(); + virtual BOOL PreTranslateMessage(MSG* pMsg); + afx_msg void OnBnClickedButtonJoinchannel(); + afx_msg void OnSelchangeListInfoBroadcasting(); + +}; diff --git a/windows/APIExample/APIExample/Advanced/ScreenShare/AgoraScreenCapture.cpp b/windows/APIExample/APIExample/Advanced/ScreenShare/AgoraScreenCapture.cpp new file mode 100644 index 000000000..47187396c --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/ScreenShare/AgoraScreenCapture.cpp @@ -0,0 +1,894 @@ +#include "stdafx.h" +#include "APIExample.h" +#include "AgoraScreenCapture.h" +#include +#pragma comment(lib,"dwmapi.lib") + +IMPLEMENT_DYNAMIC(CAgoraScreenCapture, CDialogEx) + +CAgoraScreenCapture::CAgoraScreenCapture(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_SCREEN_SHARE, pParent) +{ + +} + +CAgoraScreenCapture::~CAgoraScreenCapture() +{ +} + +void CAgoraScreenCapture::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannel); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannel); + DDX_Control(pDX, IDC_STATIC_SCREEN_CAPTURE, m_staScreenCap); + DDX_Control(pDX, IDC_COMBO_SCREEN_CAPTURE, m_cmbScreenCap); + DDX_Control(pDX, IDC_BUTTON_START_CAPUTRE, m_btnStartCap); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); + DDX_Control(pDX, IDC_CHECK_CURSOR, m_chkShareCursor); + DDX_Control(pDX, IDC_EDIT_FPS, m_edtFPS); + DDX_Control(pDX, IDC_EDIT_BITRATE, m_edtBitrate); + DDX_Control(pDX, IDC_COMBO_SCREEN_SCREEN, m_cmbScreenRegion); + + + DDX_Control(pDX, IDC_BUTTON_START_SHARE_SCREEN, m_btnShareScreen); + DDX_Control(pDX, IDC_STATIC_SCREEN_INFO, m_staScreenInfo); + DDX_Control(pDX, IDC_STATIC_SCREEN_INFO2, m_staScreenInfo2); + DDX_Control(pDX, IDC_STATIC_FPS, m_staFPS); + DDX_Control(pDX, IDC_STATIC_BITRATE, m_staBitrate); + DDX_Control(pDX, IDC_STATIC_GENERAL, m_staGeneral); + DDX_Control(pDX, IDC_BUTTON_UPDATEPARAM, m_btnUpdateCaptureParam); + DDX_Control(pDX, IDC_STATIC_SCREEN_SHARE, m_StaScreen); + DDX_Control(pDX, IDC_COMBO_EXLUDE_WINDOW_LIST, m_cmbExcluedWndList); + DDX_Control(pDX, IDC_STATIC_WND_LIST, m_staExcludeWndList); + DDX_Control(pDX, IDC_CHECK_WINDOW_FOCUS, m_chkWndFocus); + DDX_Control(pDX, IDC_STATIC_DETAIL, m_staDetails); +} +//set control text from config. +void CAgoraScreenCapture::InitCtrlText() +{ + m_StaScreen.SetWindowText(screenShareCtrlScreen); + m_staFPS.SetWindowText(screenShareCtrlFPS); + m_staBitrate.SetWindowText(screenShareCtrlBitrate); + m_chkShareCursor.SetWindowText(screenShareCtrlShareCursor); + m_staGeneral.SetWindowText(screenShareCtrlGeneralSettings); + m_btnUpdateCaptureParam.SetWindowText(screenShareCtrlUpdateCaptureParam); + m_btnShareScreen.SetWindowText(screenShareCtrlShareSCreen); + m_staScreenCap.SetWindowText(screenShareCtrlScreenCap); + m_btnStartCap.SetWindowText(screenShareCtrlStartCap); + m_staChannel.SetWindowText(commonCtrlChannel); + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + m_staExcludeWndList.SetWindowText(screenShareCtrlExcludeWindowList); + m_chkWndFocus.SetWindowText(screenShareCtrlWindowFocus); +} + +//Initialize the Agora SDK +bool CAgoraScreenCapture::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //enable video in the engine. + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + m_btnJoinChannel.EnableWindow(TRUE); + return true; +} +//UnInitialize the Agora SDK +void CAgoraScreenCapture::UnInitAgora() +{ + if (m_rtcEngine) { + if(m_joinChannel) + m_joinChannel = !m_rtcEngine->leaveChannel(); + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} +//render local video from SDK local capture. +void CAgoraScreenCapture::RenderLocalVideo() +{ + if (m_rtcEngine) { + //start preview in the engine. + m_rtcEngine->startPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("startPreview")); + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_localVideoWnd.GetSafeHwnd(); + //setup local video in the engine to canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setupLocalVideo")); + } +} + + +//EID_JOINCHANNEL_SUCCESS message window handler. +LRESULT CAgoraScreenCapture::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_btnJoinChannel.EnableWindow(TRUE); + m_btnStartCap.EnableWindow(TRUE); + m_joinChannel = true; + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_localVideoWnd.SetUID(wParam); + + //notify parent window + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); + return 0; +} +//EID_LEAVE_CHANNEL message window handler. +LRESULT CAgoraScreenCapture::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + m_btnJoinChannel.EnableWindow(TRUE); + m_btnStartCap.EnableWindow(FALSE); + + m_joinChannel = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + + CString strInfo; + strInfo.Format(_T("leave channel success %s"), getCurrentTime()); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return 0; +} +//EID_USER_JOINED message window handler. +LRESULT CAgoraScreenCapture::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + CString strInfo; + strInfo.Format(_T("%u joined"), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + + return 0; +} +//EID_USER_OFFLINE message window handler. +LRESULT CAgoraScreenCapture::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + //setup remote video in the engine to canvas. + m_rtcEngine->setupRemoteVideo(canvas); + CString strInfo; + strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + +//EID_REMOTE_VIDEO_STATE_CHANED message window handler. +LRESULT CAgoraScreenCapture::OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam) +{ + PVideoStateStateChanged stateChanged = (PVideoStateStateChanged)wParam; + if (stateChanged) { + //onRemoteVideoStateChanged + + CString strSateInfo; + switch (stateChanged->state) { + case REMOTE_VIDEO_STATE_STARTING: + strSateInfo = _T("REMOTE_VIDEO_STATE_STARTING"); + break; + case REMOTE_VIDEO_STATE_STOPPED: + strSateInfo = _T("strSateInfo"); + break; + case REMOTE_VIDEO_STATE_DECODING: + strSateInfo = _T("REMOTE_VIDEO_STATE_DECODING"); + break; + case REMOTE_VIDEO_STATE_FAILED: + strSateInfo = _T("REMOTE_VIDEO_STATE_FAILED "); + break; + case REMOTE_VIDEO_STATE_FROZEN: + strSateInfo = _T("REMOTE_VIDEO_STATE_FROZEN "); + break; + } + + CString strInfo; + strInfo.Format(_T("onRemoteVideoStateChanged: uid=%u, %s"), stateChanged->uid, strSateInfo); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + return 0; +} + +LRESULT CAgoraScreenCapture::OnEIDLocalVideoStateChanged(WPARAM wParam, LPARAM lParam) +{ + LOCAL_VIDEO_STREAM_STATE localVideoState =(LOCAL_VIDEO_STREAM_STATE) wParam; + LOCAL_VIDEO_STREAM_ERROR error = (LOCAL_VIDEO_STREAM_ERROR)lParam; + CString strState; + CString strError; + CString strInfo; + switch (localVideoState) + { + case agora::rtc::LOCAL_VIDEO_STREAM_STATE_STOPPED: + strState = _T("LOCAL_VIDEO_STREAM_STATE_STOPPED"); + break; + case agora::rtc::LOCAL_VIDEO_STREAM_STATE_CAPTURING: + strState = _T("LOCAL_VIDEO_STREAM_STATE_CAPTURING"); + break; + case agora::rtc::LOCAL_VIDEO_STREAM_STATE_ENCODING: + strState = _T("LOCAL_VIDEO_STREAM_STATE_ENCODING"); + break; + case agora::rtc::LOCAL_VIDEO_STREAM_STATE_FAILED: + strState = _T("LOCAL_VIDEO_STREAM_STATE_FAILED"); + break; + default: + strState = _T("UNKNOW STATE"); + break; + } + switch (error) + { + + case agora::rtc::LOCAL_VIDEO_STREAM_ERROR_OK: + strError = _T("LOCAL_VIDEO_STREAM_ERROR_OK"); + break; + case agora::rtc::LOCAL_VIDEO_STREAM_ERROR_FAILURE: + strError = _T("LOCAL_VIDEO_STREAM_ERROR_FAILURE"); + + break; + case agora::rtc::LOCAL_VIDEO_STREAM_ERROR_DEVICE_NO_PERMISSION: + strError = _T("LOCAL_VIDEO_STREAM_ERROR_DEVICE_NO_PERMISSION"); + + break; + case agora::rtc::LOCAL_VIDEO_STREAM_ERROR_DEVICE_BUSY: + strError = _T("LOCAL_VIDEO_STREAM_ERROR_DEVICE_BUSY"); + + break; + case agora::rtc::LOCAL_VIDEO_STREAM_ERROR_CAPTURE_FAILURE: + strError = _T("LOCAL_VIDEO_STREAM_ERROR_CAPTURE_FAILURE"); + break; + case agora::rtc::LOCAL_VIDEO_STREAM_ERROR_ENCODE_FAILURE: + strError = _T("LOCAL_VIDEO_STREAM_ERROR_ENCODE_FAILURE"); + break; + case agora::rtc::LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_WINDOW_MINIMIZED: + strError = _T("LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_WINDOW_MINIMIZED"); + break; + case agora::rtc::LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_WINDOW_CLOSED: + strError = _T("LOCAL_VIDEO_STREAM_ERROR_SCREEN_CAPTURE_WINDOW_MINIMIZED"); + break; + default: + strError = _T("UNKNOW ERROR"); + break; + } + strInfo.Format(_T("onLocalVideoStateChanged state:\n%s: error:\n%s"), strState, strError); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return TRUE; +} + + + +BEGIN_MESSAGE_MAP(CAgoraScreenCapture, CDialogEx) + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraScreenCapture::OnBnClickedButtonJoinchannel) + ON_BN_CLICKED(IDC_BUTTON_START_CAPUTRE, &CAgoraScreenCapture::OnBnClickedButtonStartShare) + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraScreenCapture::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraScreenCapture::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraScreenCapture::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraScreenCapture::OnEIDUserOffline) + ON_MESSAGE(WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), &CAgoraScreenCapture::OnEIDRemoteVideoStateChanged) + ON_MESSAGE(WM_MSGID(EID_LOCAL_VIDEO_STATE_CHANGED), &CAgoraScreenCapture::OnEIDLocalVideoStateChanged) + + ON_WM_SHOWWINDOW() + ON_BN_CLICKED(IDC_BUTTON_UPDATEPARAM, &CAgoraScreenCapture::OnBnClickedButtonUpdateparam) + // ON_BN_CLICKED(IDC_BUTTON_SHARE_DESKTOP, &CAgoraScreenCapture::OnBnClickedButtonShareDesktop) + // ON_CBN_SELCHANGE(IDC_COMBO_SCREEN_REGION, &CAgoraScreenCapture::OnCbnSelchangeComboScreenRegion) + ON_BN_CLICKED(IDC_BUTTON_START_SHARE_SCREEN, &CAgoraScreenCapture::OnBnClickedButtonStartShareScreen) + ON_LBN_SELCHANGE(IDC_LIST_INFO_BROADCASTING, &CAgoraScreenCapture::OnSelchangeListInfoBroadcasting) +END_MESSAGE_MAP() + + + +/* + initialize dialog, and set control property. +*/ +BOOL CAgoraScreenCapture::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + m_localVideoWnd.MoveWindow(&rcArea); + m_localVideoWnd.ShowWindow(SW_SHOW); + ResumeStatus(); + InitMonitorInfos(); + return TRUE; +} + +void CAgoraScreenCapture::InitMonitorInfos() +{ + m_monitors.EnumMonitor(); + + std::vector infos = m_monitors.GetMonitors(); + CString str = _T(""); + for (size_t i = 0; i < infos.size(); i++) { + RECT rcMonitor = infos[i].monitorInfo.rcMonitor; + CString strInfo; + strInfo.Format(_T("Screen%d: rect = {%d, %d, %d, %d} ") + , i + 1, rcMonitor.left, rcMonitor.top, rcMonitor.right, rcMonitor.bottom); + if (rcMonitor.left < 0 || rcMonitor.top < 0) {//negative coordinate is not supported + strInfo += _T("not support negative cordinate;"); + str += strInfo; + continue; + } + str += strInfo; + m_cmbScreenRegion.InsertString(i, utf82cs(infos[i].monitorName)); + } + + m_cmbScreenRegion.InsertString(infos.size(), _T("Select Window Hwnd Rect Area")); + m_staScreenInfo.SetWindowText(str); + m_cmbScreenRegion.SetCurSel(0); +} + +//The JoinChannel button's click handler. +//This function either joins or leaves the channel +void CAgoraScreenCapture::OnBnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + CString strInfo; + if (!m_joinChannel) { + CString strChannelName; + m_edtChannel.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + + std::string szChannelId = cs2utf8(strChannelName); + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + strInfo.Format(_T("join channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + } + } + else { + if (0 == m_rtcEngine->leaveChannel()) { + strInfo.Format(_T("leave channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + } + } + + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + +// start or stop screen capture. +void CAgoraScreenCapture::OnBnClickedButtonStartShare() +{ + if (!m_rtcEngine || !m_initialize) + return; + HWND hWnd = NULL; + //if (m_cmbScreenCap.GetCurSel() != m_cmbScreenCap.GetCount() - 1) + hWnd = m_listWnd.GetAt(m_listWnd.FindIndex(m_cmbScreenCap.GetCurSel())); + int ret = 0; + m_windowShare = !m_windowShare; + if (m_windowShare) + { + //::SwitchToThisWindow(hWnd, TRUE); + //start screen capture in the engine. + ScreenCaptureParameters capParam; + GetCaptureParameterFromCtrl(capParam); + CRect rcWnd = { 0 }; + ::GetClientRect(hWnd, &rcWnd); + agora::rtc::Rectangle rcCapWnd = { rcWnd.left, rcWnd.top, rcWnd.right - rcWnd.left, rcWnd.bottom - rcWnd.top }; + + ret = m_rtcEngine->startScreenCaptureByWindowId(hWnd, rcCapWnd, capParam); + + + if (ret == 0) + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("start share window succees!")); + else + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("start share window failed!")); + + m_btnStartCap.SetWindowText(screenShareCtrlEndCap); + + m_btnShareScreen.EnableWindow(FALSE); + + } + else { + //stop screen capture in the engine. + ret = m_rtcEngine->stopScreenCapture(); + if (ret == 0) + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stop screen share stop succees!")); + else + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stop screen share stop failed!")); + m_btnStartCap.SetWindowText(screenShareCtrlStartCap); + m_btnShareScreen.EnableWindow(TRUE); + } +} + +// render local video and refresh zoomed window add m_cmbScreenCap. +void CAgoraScreenCapture::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow) { + RenderLocalVideo(); + ReFreshWnd(); + } + else { + ResumeStatus(); + } +} + +// call RefreashWndInfo to refresh window list and to m_cmbScreenCap. +void CAgoraScreenCapture::ReFreshWnd() +{ + //refresh window info. + RefreashWndInfo(); + POSITION pos = m_listWnd.GetHeadPosition(); + HWND hWnd = NULL; + TCHAR strName[255]; + m_cmbExcluedWndList.InsertString(0, _T("no exclued window.")); + int index = 0; + //enumerate hwnd to add m_cmbScreenCap. + while (pos != NULL) { + hWnd = m_listWnd.GetNext(pos); + ::GetWindowText(hWnd, strName, 255); + m_cmbScreenCap.InsertString(index++, strName); + m_cmbExcluedWndList.InsertString(index, strName); + } + //m_cmbScreenCap.InsertString(index++, L"DeskTop"); + m_cmbScreenCap.SetCurSel(0); + m_cmbExcluedWndList.SetCurSel(0); +} + +//Get ScreenCaptureParameters from ctrl +void CAgoraScreenCapture::GetCaptureParameterFromCtrl(agora::rtc::ScreenCaptureParameters& capParam) +{ + capParam.captureMouseCursor = m_chkShareCursor.GetCheck(); + static view_t excludeWnd[2]; + CString str; + m_edtFPS.GetWindowText(str); + if (str.IsEmpty()) + capParam.frameRate = 15; //default fps + else + capParam.frameRate = _ttoi(str); + HWND hWnd = NULL; + if (m_cmbScreenCap.GetCurSel() > 0) + hWnd = m_listWnd.GetAt(m_listWnd.FindIndex(m_cmbScreenCap.GetCurSel())); + excludeWnd[0] = hWnd; + capParam.excludeWindowList = excludeWnd; + capParam.windowFocus = m_chkWndFocus.GetCheck(); + capParam.excludeWindowCount = 1; + str.Empty(); + m_edtBitrate.GetWindowText(str); + if (!str.IsEmpty()) + capParam.bitrate = _ttoi(str); + else + capParam.bitrate = 0;//default + +} + +//resume window status +void CAgoraScreenCapture::ResumeStatus() +{ + m_lstInfo.ResetContent(); + InitCtrlText(); + m_joinChannel = false; + m_initialize = false; + m_addInjectStream = false; + m_windowShare = false; + m_screenShare = false; + m_edtChannel.SetWindowText(_T("")); + m_cmbScreenCap.ResetContent(); + + m_chkShareCursor.SetCheck(TRUE); + m_chkWndFocus.SetCheck(TRUE); + m_edtFPS.SetWindowText(_T("15")); + m_edtBitrate.SetWindowText(_T("")); +} + +void CScreenCaptureEventHandler::onLocalVideoStateChanged(LOCAL_VIDEO_STREAM_STATE localVideoState, LOCAL_VIDEO_STREAM_ERROR error) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LOCAL_VIDEO_STATE_CHANGED), (WPARAM)localVideoState, (LPARAM)error); + } +} + +/* +note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one +parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). +*/ +void CScreenCaptureEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } +} +/* +note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. +parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). +*/ +void CScreenCaptureEventHandler::onUserJoined(uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } +} + +/* +note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. +parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. +*/ +void CScreenCaptureEventHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } +} +/* +note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. +parameters: + stats: Call statistics. +*/ + +void CScreenCaptureEventHandler::onLeaveChannel(const RtcStats& stats) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } +} +/** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. +*/ +void CScreenCaptureEventHandler::onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) +{ + if (m_hMsgHanlder) { + PVideoStateStateChanged stateChanged = new VideoStateStateChanged; + stateChanged->uid = uid; + stateChanged->reason = reason; + stateChanged->state = state; + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), (WPARAM)stateChanged, 0); + } +} + +static +BOOL IsWindowCloaked(HWND hwnd) +{ + BOOL isCloaked = FALSE; + return (SUCCEEDED(DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, + &isCloaked, sizeof(isCloaked))) && isCloaked); +} +static +BOOL IsWindowVisibleOnScreen(HWND hwnd) +{ + return IsWindowVisible(hwnd) && + !IsWindowCloaked(hwnd); +} + +/* + enum window callback function. +*/ +BOOL CALLBACK CAgoraScreenCapture::WndEnumProc(HWND hWnd, LPARAM lParam) +{ + CList* lpListctrl = (CList*)lParam; + TCHAR strName[255]; + ::GetWindowText(hWnd, strName, 255); + CString str = strName; + LONG lStyle = ::GetWindowLong(hWnd, GWL_STYLE); + if ((lStyle & WS_VISIBLE) != 0 + && (lStyle & (WS_POPUP | WS_SYSMENU)) != 0 + && IsWindowVisibleOnScreen(hWnd) + && !str.IsEmpty() + && str.Compare(_T("Program Manager")) + //&&::IsZoomed(hWnd) + ) + lpListctrl->AddTail(hWnd); + + return TRUE; +} + +/* + refresh zoomed window.and add to m_listWnd. +*/ +int CAgoraScreenCapture::RefreashWndInfo() +{ + m_listWnd.RemoveAll(); + ::EnumWindows(&CAgoraScreenCapture::WndEnumProc, (LPARAM)&m_listWnd); + return static_cast(m_listWnd.GetCount()); +} + + +BOOL CAgoraScreenCapture::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} + + +void CAgoraScreenCapture::OnBnClickedButtonUpdateparam() +{ + ScreenCaptureParameters capParam; + GetCaptureParameterFromCtrl(capParam); + m_rtcEngine->updateScreenCaptureParameters(capParam); +} + + +CMonitors::CMonitors() +{ + +} +CMonitors::~CMonitors() +{ + +} + + +BOOL CMonitors::MonitorFunc(HMONITOR hMonitor, HDC hDc, LPRECT lpRect, LPARAM lParam) +{ + CMonitors* pThis = (CMonitors*)lParam; + + MONITORINFOEX info; + info.cbSize = sizeof(MONITORINFOEX); + GetMonitorInfo(hMonitor, &info); + + DEVMODE devMode; + devMode.dmSize = sizeof(DEVMODE); + EnumDisplaySettings(info.szDevice, ENUM_CURRENT_SETTINGS, &devMode); + + DEVICE_SCALE_FACTOR scale; + HRESULT hr = GetScaleFactorForMonitor(hMonitor, &scale); + MonitorInformation monitorInfo; + monitorInfo.monitorInfo = info; + monitorInfo.hMonitor = hMonitor; + if (info.rcMonitor.left < 0 + || info.rcMonitor.top < 0) { + monitorInfo.canShare = false; + } + // UINT dpiX = 0; + // UINT dpiY = 0; + // GetDpiForMonitor(hMonitor, MDT_EFFECTIVE_DPI ,&dpiX, &dpiY); + pThis->m_vecMonitorInfos.push_back(monitorInfo); + if(monitorInfo.canShare) + pThis->m_vecEffectiveMonitorInfos.push_back(monitorInfo); + return TRUE; +} + +void CMonitors::Clear() +{ + m_vecMonitorInfos.clear(); + m_screenRegion = { 0,0,0,0 }; +} + +void CMonitors::EnumMonitor() +{ + Clear(); + EnumDisplayMonitors(NULL, NULL, (MONITORENUMPROC)MonitorFunc, (LPARAM)this); + + for (size_t i = 0; i < m_vecMonitorInfos.size(); i++) { + MonitorInformation& monitorInfo = m_vecMonitorInfos[i]; + char szName[MAX_PATH] = { 0 }; + sprintf_s(szName, MAX_PATH, "Screen%d", i + 1); + monitorInfo.monitorName = szName; + } + + if (m_vecMonitorInfos.size() == 1) { + m_screenRegion = m_vecMonitorInfos[0].monitorInfo.rcMonitor; + } + else { + m_screenRegion.left = GetSystemMetrics(SM_XVIRTUALSCREEN); + m_screenRegion.top = GetSystemMetrics(SM_YVIRTUALSCREEN); + m_screenRegion.right = m_screenRegion.left + GetSystemMetrics(SM_CXVIRTUALSCREEN); + m_screenRegion.bottom = m_screenRegion.top + GetSystemMetrics(SM_CYVIRTUALSCREEN); + } +} + +agora::rtc::Rectangle CMonitors::GetScreenRect() +{ + return RectToRectangle(m_screenRegion); +} + +agora::rtc::Rectangle CMonitors::RectToRectangle(RECT rc) +{ + agora::rtc::Rectangle agoraRect = { rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top }; + + return agoraRect; +} + +bool CMonitors::IsValid() +{ + int left = GetSystemMetrics(SM_XVIRTUALSCREEN); + int top = GetSystemMetrics(SM_YVIRTUALSCREEN); + + if (left < 0 || top < 0) { + return false; + } + + return true; +} + +CMonitors::MonitorInformation CMonitors::GetMonitorInformation(int index) +{ + return m_vecEffectiveMonitorInfos[index]; +} + +agora::rtc::Rectangle CMonitors::GetMonitorRectangle(int index) +{ + MonitorInformation& info = m_vecEffectiveMonitorInfos[index]; + RECT rc = info.monitorInfo.rcMonitor; + + agora::rtc::Rectangle rcAgora = { 0, 0, (rc.right - rc.left)*info.scale_num / info.scale_den, (rc.bottom - rc.top)*info.scale_num / info.scale_den }; + rcAgora.x = rc.left * info.scale_num / info.scale_den; + rcAgora.y = rc.top * info.scale_num / info.scale_den; + return rcAgora; +} + +bool CMonitors::CheckMonitorValid(HMONITOR hMonitor) +{ + for (size_t i = 0; i < m_vecEffectiveMonitorInfos.size(); i++){ + MonitorInformation info = m_vecEffectiveMonitorInfos[i]; + if (info.hMonitor == hMonitor) { + return info.canShare; + } + } + + return false; +} + +bool CMonitors::GetMonitorRectangle(HMONITOR hMonitor, agora::rtc::Rectangle& screenRegion) +{ + for (size_t i = 0; i < m_vecEffectiveMonitorInfos.size(); i++) { + MonitorInformation info = m_vecEffectiveMonitorInfos[i]; + if (info.hMonitor == hMonitor) { + screenRegion = RectToRectangle(info.monitorInfo.rcMonitor); + return true; + } + } + return false; +} + +bool CMonitors::GetWindowRect(HWND hWnd, agora::rtc::Rectangle& regionRect) +{ + if (!hWnd) + return false; + + HMONITOR hMonitor = MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST); + RECT rcWnd = { 0 }; + ::GetWindowRect(hWnd, &rcWnd); + + agora::rtc::Rectangle rcScreen; + GetMonitorRectangle(hMonitor, rcScreen); + + regionRect = RectToRectangle(rcWnd); + regionRect.x = rcWnd.left - rcScreen.x; + regionRect.y = rcWnd.top - rcScreen.y; + return true; +} + +void CAgoraScreenCapture::OnBnClickedButtonStartShareScreen() +{ + m_screenShare = !m_screenShare; + if (m_screenShare) { + int sel = m_cmbScreenRegion.GetCurSel(); + agora::rtc::Rectangle regionRect = { 0,0,0,0 }, screenRegion = {0,0,0,0}; + if (sel < m_monitors.GetMonitorCount()) + {//share screen rect area + regionRect = m_monitors.GetMonitorRectangle(sel); + screenRegion = m_monitors.GetScreenRect(); + } + else { + // get selected window HWND + if (m_cmbScreenCap.GetCurSel() != m_cmbScreenCap.GetCount() - 1) { + HWND hWnd = NULL; + hWnd = m_listWnd.GetAt(m_listWnd.FindIndex(m_cmbScreenCap.GetCurSel())); + HMONITOR hMonitor = MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST); + if (!m_monitors.CheckMonitorValid(hMonitor)) { + AfxMessageBox(_T("The monitor that window is located in can not be shared.\nThe monitor rect area has negative cordinate.")); + return; + } + + m_monitors.GetMonitorRectangle(hMonitor, screenRegion); + m_monitors.GetWindowRect(hWnd, regionRect); + } + } + + m_monitors.GetScreenRect(); + ScreenCaptureParameters capParam; + if (regionRect.x < 0 || regionRect.y < 0) { + AfxMessageBox(_T("select hwnd rect has minus location")); + return; + } + m_rtcEngine->startScreenCaptureByScreenRect(screenRegion, regionRect, capParam); + m_btnShareScreen.SetWindowText(screenShareCtrlStopShare); + + m_btnStartCap.EnableWindow(FALSE); + + } + else { + m_rtcEngine->stopScreenCapture(); + m_btnShareScreen.SetWindowText(screenShareCtrlShareSCreen); + m_btnStartCap.EnableWindow(TRUE); + } +} + + + +void CAgoraScreenCapture::OnSelchangeListInfoBroadcasting() +{ + int sel = m_lstInfo.GetCurSel(); + if (sel < 0)return; + CString strDetail; + m_lstInfo.GetText(sel, strDetail); + m_staDetails.SetWindowText(strDetail); +} diff --git a/windows/APIExample/APIExample/Advanced/ScreenShare/AgoraScreenCapture.h b/windows/APIExample/APIExample/Advanced/ScreenShare/AgoraScreenCapture.h new file mode 100644 index 000000000..4d52508c3 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/ScreenShare/AgoraScreenCapture.h @@ -0,0 +1,212 @@ +#include "stdafx.h" +#include"AGVideoWnd.h" + + +class CScreenCaptureEventHandler : public IRtcEngineEventHandler +{ +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + + /** Occurs when the local video stream state changes. + This callback indicates the state of the local video stream, including camera capturing and video encoding, and allows you to troubleshoot issues when exceptions occur. + @note For some device models, the SDK will not trigger this callback when the state of the local video changes while the local video capturing device is in use, so you have to make your own timeout judgment. + @param localVideoState State type #LOCAL_VIDEO_STREAM_STATE. When the state is LOCAL_VIDEO_STREAM_STATE_FAILED (3), see the `error` parameter for details. + @param error The detailed error information: #LOCAL_VIDEO_STREAM_ERROR. + */ + virtual void onLocalVideoStateChanged(LOCAL_VIDEO_STREAM_STATE localVideoState, LOCAL_VIDEO_STREAM_ERROR error) override; + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override; + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override; + /** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. + */ + virtual void onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) override; +private: + HWND m_hMsgHanlder; +}; + + +class CMonitors { +public: + typedef struct _MonitorInformation { + MONITORINFOEX monitorInfo; + int scale_num = 4; + int scale_den = 4; + std::string monitorName = ""; + bool canShare = true; + HMONITOR hMonitor; + }MonitorInformation, *PMonitorInformation, *LPMonitorInformation; + CMonitors(); + ~CMonitors(); + static BOOL MonitorFunc(HMONITOR hMonitor, HDC hDc, LPRECT lpRect, LPARAM lParam); + void EnumMonitor(); + agora::rtc::Rectangle RectToRectangle(RECT rc); + std::vector GetMonitors() { return m_vecMonitorInfos; } + MonitorInformation GetMonitorInformation(int index); + agora::rtc::Rectangle GetMonitorRectangle(int index); + bool GetMonitorRectangle(HMONITOR hMonitor, agora::rtc::Rectangle& screenRegion); + agora::rtc::Rectangle GetScreenRect(); + int GetMonitorCount() { return m_vecMonitorInfos.size(); } + bool IsValid(); + bool CheckMonitorValid(HMONITOR hMonitor); + bool GetWindowRect(HWND hWnd, agora::rtc::Rectangle& regionRect); +private: + void Clear(); + std::vector m_vecMonitorInfos; + std::vector m_vecEffectiveMonitorInfos; + RECT m_screenRegion; + +}; + +class CAgoraScreenCapture : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraScreenCapture) + +public: + CAgoraScreenCapture(CWnd* pParent = nullptr); + virtual ~CAgoraScreenCapture(); + + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //set control text from config. + void InitCtrlText(); + //render local video from SDK local capture. + void RenderLocalVideo(); + //callback window enum. + static BOOL CALLBACK WndEnumProc(HWND hWnd, LPARAM lParam); + //refresh window to show. + void ReFreshWnd(); + //resume window + void ResumeStatus(); + //refresh window info to list. + int RefreashWndInfo(); + + enum { IDD = IDD_DIALOG_SCREEN_SHARE }; + afx_msg LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnEIDLocalVideoStateChanged(WPARAM wParam, LPARAM lParam); + +protected: + virtual void DoDataExchange(CDataExchange* pDX); + DECLARE_MESSAGE_MAP() + + CAGVideoWnd m_localVideoWnd; + CList m_listWnd; + CScreenCaptureEventHandler m_eventHandler; + + IRtcEngine* m_rtcEngine = nullptr; + bool m_joinChannel = false; + bool m_initialize = false; + bool m_addInjectStream = false; + bool m_windowShare = false; + bool m_screenShare = false; + CMonitors m_monitors; + + void GetCaptureParameterFromCtrl(agora::rtc::ScreenCaptureParameters& capParam); + void InitMonitorInfos(); +public: + virtual BOOL OnInitDialog(); + afx_msg void OnBnClickedButtonJoinchannel(); + afx_msg void OnBnClickedButtonStartShare(); + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + + CStatic m_staVideoArea; + CStatic m_staChannel; + CEdit m_edtChannel; + CStatic m_staScreenCap; + CComboBox m_cmbScreenCap; + CButton m_btnStartCap; + CButton m_btnJoinChannel; + CListBox m_lstInfo; + virtual BOOL PreTranslateMessage(MSG* pMsg); + afx_msg void OnBnClickedButtonUpdateparam(); + CComboBox m_cmbScreenRegion; + CButton m_chkShareCursor; + CEdit m_edtFPS; + CEdit m_edtBitrate; + //afx_msg void OnBnClickedButtonShareDesktop(); + //afx_msg void OnCbnSelchangeComboScreenRegion(); + CComboBox m_cmbShareDesktopRect; + CComboBox m_cmbShareRegion; + afx_msg void OnBnClickedButtonStartShareScreen(); + CButton m_btnShareScreen; + CStatic m_staScreenInfo; + CStatic m_staScreenInfo2; + CStatic m_staFPS; + CStatic m_staBitrate; + CStatic m_staGeneral; + CButton m_btnUpdateCaptureParam; + CStatic m_StaScreen; + CComboBox m_cmbExcluedWndList; + CStatic m_staExcludeWndList; + CButton m_chkWndFocus; + afx_msg void OnSelchangeListInfoBroadcasting(); + CStatic m_staDetails; +}; diff --git a/windows/APIExample/APIExample/Advanced/VideoMetadata/CAgoraMetaDataDlg.cpp b/windows/APIExample/APIExample/Advanced/VideoMetadata/CAgoraMetaDataDlg.cpp new file mode 100644 index 000000000..8509fdf7d --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/VideoMetadata/CAgoraMetaDataDlg.cpp @@ -0,0 +1,524 @@ +// CAgoraMetaDataDlg.cpp : implementation file + + +#include "stdafx.h" +#include "APIExample.h" +#include "CAgoraMetaDataDlg.h" +#include +//metadata +std::mutex g_mtxData; +//set max meta data size. +void CAgoraMetaDataObserver::SetMaxMetadataSize(int maxSize) +{ + if (maxSize > 1024) + maxSize = 1024; + m_maxSize = maxSize; +} +/* + get max meta data size of byte. +*/ +int CAgoraMetaDataObserver::getMaxMetadataSize() +{ + return m_maxSize; +} +/* + The sender is ready to send MetadataThis callback method + is triggered when the SDK is ready to receive and send Metadata. + annotations: + Make sure that the Metadata size passed in to this method does not + exceed the value set in getMaxMetadataSize. + parameter: + metadata :Metadata that the user wants to send. + return: + True: send + False: don't send +*/ +bool CAgoraMetaDataObserver::onReadyToSendMetadata(Metadata &metadata) +{ + std::lock_guard lockSendData(g_mtxData); + if (m_sendSEI.length() > 0) { + memcpy_s(metadata.buffer, m_sendSEI.length(), m_sendSEI.c_str(), m_sendSEI.length()); + + } + metadata.size = m_sendSEI.length(); + return true; +} +/* + The receiver has received Metadata.The SDK triggers the callback when it + receives Metadata sent by the remote user. + parameter: + metadata:Received Metadata. +*/ +void CAgoraMetaDataObserver::onMetadataReceived(const Metadata &metadata) +{ + if (m_hMsgHanlder) { + Metadata* recvMetaData = new Metadata; + recvMetaData->size = metadata.size; + recvMetaData->uid = metadata.uid; + recvMetaData->timeStampMs = metadata.timeStampMs; + if (metadata.size > 0) { + recvMetaData->buffer = new unsigned char[metadata.size]; + memcpy_s(recvMetaData->buffer, metadata.size, metadata.buffer, metadata.size); + recvMetaData->buffer[metadata.size] = 0; + } + ::PostMessage(m_hMsgHanlder, WM_MSGID(RECV_METADATA_MSG), (WPARAM)recvMetaData, 0); + } +} +//set send string. +void CAgoraMetaDataObserver::SetSendSEI(std::string utf8Msg) +{ + std::lock_guard lockSendData(g_mtxData); + m_sendSEI = utf8Msg; + +} +/* +note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one +parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). +*/ +void CAgoraMetaDataEventHanlder::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } +} +/* +note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. +parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). +*/ +void CAgoraMetaDataEventHanlder::onUserJoined(uid_t uid, int elapsed) { + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } +} +/* +note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. +parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. +*/ +void CAgoraMetaDataEventHanlder::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } +} +/* +note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. +parameters: + stats: Call statistics. +*/ +void CAgoraMetaDataEventHanlder::onLeaveChannel(const RtcStats& stats) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } +} +/** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. +*/ +void CAgoraMetaDataEventHanlder::onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) +{ + if (m_hMsgHanlder) { + PVideoStateStateChanged stateChanged = new VideoStateStateChanged; + stateChanged->uid = uid; + stateChanged->reason = reason; + stateChanged->state = state; + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), (WPARAM)stateChanged, 0); + } +} + +// CAgoraMetaDataDlg dialog + +IMPLEMENT_DYNAMIC(CAgoraMetaDataDlg, CDialogEx) + +CAgoraMetaDataDlg::CAgoraMetaDataDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_METADATA, pParent) +{ + +} + +CAgoraMetaDataDlg::~CAgoraMetaDataDlg() +{ +} + +void CAgoraMetaDataDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannelName); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_STATIC_SENDSEI, m_staSendSEI); + DDX_Control(pDX, IDC_EDIT_SEI, m_edtSendSEI); + DDX_Control(pDX, IDC_EDIT_RECV, m_edtRecvSEI); + DDX_Control(pDX, IDC_STATIC_METADATA_INFO, m_staMetaData); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannelName); + DDX_Control(pDX, IDC_BUTTON_SEND, m_btnSendSEI); + DDX_Control(pDX, IDC_BUTTON_CLEAR, m_btnClear); +} + + +BEGIN_MESSAGE_MAP(CAgoraMetaDataDlg, CDialogEx) + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraMetaDataDlg::OnBnClickedButtonJoinchannel) + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraMetaDataDlg::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraMetaDataDlg::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraMetaDataDlg::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraMetaDataDlg::OnEIDUserOffline) + ON_MESSAGE(WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), &CAgoraMetaDataDlg::OnEIDRemoteVideoStateChanged) + ON_MESSAGE(WM_MSGID(RECV_METADATA_MSG), &CAgoraMetaDataDlg::OnEIDMetadataReceived) + ON_WM_SHOWWINDOW() + ON_BN_CLICKED(IDC_BUTTON_SEND, &CAgoraMetaDataDlg::OnBnClickedButtonSend) + ON_BN_CLICKED(IDC_BUTTON_CLEAR, &CAgoraMetaDataDlg::OnBnClickedButtonClear) +END_MESSAGE_MAP() + + +// CAgoraMetaDataDlg message handlers + + +void CAgoraMetaDataDlg::OnBnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + CString strInfo; + if (!m_joinChannel) { + CString strChannelName; + m_edtChannelName.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + + std::string szChannelId = cs2utf8(strChannelName); + //join channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + strInfo.Format(_T("join channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + } + } + else { + //leave channel in the engine. + if (0 == m_rtcEngine->leaveChannel()) { + strInfo.Format(_T("leave channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + } + } + + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + + +BOOL CAgoraMetaDataDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + m_remoteVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + RECT rcLeft = rcArea, rcRight = rcArea; + rcLeft.right = rcLeft.left + (rcArea.right - rcArea.left) / 2; + rcRight.left = rcLeft.right + 1; + m_localVideoWnd.MoveWindow(&rcLeft); + m_remoteVideoWnd.MoveWindow(&rcRight); + m_localVideoWnd.ShowWindow(SW_SHOW); + m_remoteVideoWnd.ShowWindow(SW_SHOW); + + ResumeStatus(); + return TRUE; +} + +//set control text from config. +void CAgoraMetaDataDlg::InitCtrlText() +{ + m_btnClear.SetWindowText(metadataCtrlBtnClear); + m_staMetaData.SetWindowText(videoSEIInformation); + m_staSendSEI.SetWindowText(metadataCtrlSendSEI); + m_btnSendSEI.SetWindowText(metadataCtrlBtnSend); + m_staChannelName.SetWindowText(commonCtrlChannel); + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); +} +//Initialize the Agora SDK +bool CAgoraMetaDataDlg::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //enable video in the engine. + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + //set meta data observer notify window. + m_metaDataObserver.SetMsgReceiver(m_hWnd); + //register media meta data observer. + m_rtcEngine->registerMediaMetadataObserver(&m_metaDataObserver, IMetadataObserver::VIDEO_METADATA); + + m_btnJoinChannel.EnableWindow(TRUE); + return true; +} + +//UnInitialize the Agora SDK +void CAgoraMetaDataDlg::UnInitAgora() +{ + if (m_rtcEngine) { + if(m_joinChannel) + m_joinChannel = !m_rtcEngine->leaveChannel(); + //stop preview in the window. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} + +//EID_JOINCHANNEL_SUCCESS message window handler. +LRESULT CAgoraMetaDataDlg::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_btnJoinChannel.EnableWindow(TRUE); + m_joinChannel = true; + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_localVideoWnd.SetUID(wParam); + //notify parent window + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); + return 0; +} +//EID_LEAVE_CHANNEL message window hander. +LRESULT CAgoraMetaDataDlg::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + m_btnJoinChannel.EnableWindow(TRUE); + m_joinChannel = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + + CString strInfo; + strInfo.Format(_T("leave channel success %s"), getCurrentTime()); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + + //notify parent window + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return 0; +} + +//EID_USER_JOINED message window handler. +LRESULT CAgoraMetaDataDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + CString strInfo; + strInfo.Format(_T("%u joined"), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + + /* VideoCanvas canvas; + canvas.uid = wParam; + canvas.view = m_remoteVideoWnd.GetSafeHwnd(); + canvas.renderMode = RENDER_MODE_FIT; + m_rtcEngine->setupRemoteVideo(canvas);*/ + return 0; +} + +//EID_USER_OFFLINE message window handler. +LRESULT CAgoraMetaDataDlg::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + //set up remote video in the engine to canvas. + m_rtcEngine->setupRemoteVideo(canvas); + CString strInfo; + strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + + return 0; +} + +//EID_REMOTE_VIDEO_STATE_CHANED message window handler. +LRESULT CAgoraMetaDataDlg::OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam) +{ + PVideoStateStateChanged stateChanged = (PVideoStateStateChanged)wParam; + if (stateChanged) { + //onRemoteVideoStateChanged + + CString strSateInfo; + switch (stateChanged->state) { + case REMOTE_VIDEO_STATE_STARTING: + strSateInfo = _T("REMOTE_VIDEO_STATE_STARTING"); + break; + case REMOTE_VIDEO_STATE_STOPPED: + strSateInfo = _T("strSateInfo"); + break; + case REMOTE_VIDEO_STATE_DECODING: + strSateInfo = _T("REMOTE_VIDEO_STATE_DECODING"); + break; + case REMOTE_VIDEO_STATE_FAILED: + strSateInfo = _T("REMOTE_VIDEO_STATE_FAILED "); + break; + case REMOTE_VIDEO_STATE_FROZEN: + strSateInfo = _T("REMOTE_VIDEO_STATE_FROZEN "); + break; + } + CString strInfo; + strInfo.Format(_T("onRemoteVideoStateChanged: uid=%u, %s"), stateChanged->uid, strSateInfo); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + return 0; +} + +//RECV_METADATA_MSG message window handler. +LRESULT CAgoraMetaDataDlg::OnEIDMetadataReceived(WPARAM wParam, LPARAM lParam) +{ + IMetadataObserver::Metadata* metaData = (IMetadataObserver::Metadata*)wParam; + CString strInfo; + strInfo.Format(_T("onMetadataReceived:uid:%u, ts=%d, size:%d."), metaData->uid, metaData->timeStampMs, metaData->size, metaData->buffer); + + if (metaData->size > 0) { + CString str; + str.Format(_T("Info: %S"), metaData->buffer); + strInfo += str; + } + m_edtRecvSEI.SetWindowText(strInfo); + return 0; +} + +//render local video from SDK local capture. +void CAgoraMetaDataDlg::RenderLocalVideo() +{ + if (m_rtcEngine) { + //start preview in the engine. + m_rtcEngine->startPreview(); + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_localVideoWnd.GetSafeHwnd(); + //set up local video in the engine to canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("render local video")); + } +} + +void CAgoraMetaDataDlg::ResumeStatus() +{ + InitCtrlText(); + m_lstInfo.ResetContent(); + m_metaDataObserver.SetSendSEI(""); + m_edtChannelName.SetWindowText(_T("")); + m_edtSendSEI.SetWindowText(_T("")); + m_edtRecvSEI.SetWindowText(_T("")); + m_joinChannel = false; + m_initialize = false; + m_remoteJoined = false; +} + +void CAgoraMetaDataDlg::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + + if (bShow) { + RenderLocalVideo(); + } + else { + ResumeStatus(); + } +} + +//send button handler. +void CAgoraMetaDataDlg::OnBnClickedButtonSend() +{ + CString strSend; + m_edtSendSEI.GetWindowText(strSend); + if (strSend.IsEmpty()) + return; + std::string utf8msg = cs2utf8(strSend); + //set send message string. + m_metaDataObserver.SetSendSEI(utf8msg); +} + +//clear button handler. +void CAgoraMetaDataDlg::OnBnClickedButtonClear() +{ + m_edtSendSEI.SetWindowText(_T("")); + //set send message string. + m_metaDataObserver.SetSendSEI(""); +} + + + + +BOOL CAgoraMetaDataDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} diff --git a/windows/APIExample/APIExample/Advanced/VideoMetadata/CAgoraMetaDataDlg.h b/windows/APIExample/APIExample/Advanced/VideoMetadata/CAgoraMetaDataDlg.h new file mode 100644 index 000000000..eeaaef2f7 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/VideoMetadata/CAgoraMetaDataDlg.h @@ -0,0 +1,185 @@ +#pragma once +#include "AGVideoWnd.h" + +class CAgoraMetaDataObserver : public IMetadataObserver +{ +public: + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + /* + get max meta data size of byte. + */ + virtual int getMaxMetadataSize()override; + /* + The sender is ready to send MetadataThis callback method + is triggered when the SDK is ready to receive and send Metadata. + annotations: + Make sure that the Metadata size passed in to this method does not + exceed the value set in getMaxMetadataSize. + parameter: + metadata :Metadata that the user wants to send. + return: + True: send + False: don't send + */ + virtual bool onReadyToSendMetadata(Metadata &metadata)override; + /* + The receiver has received Metadata.The SDK triggers the callback when it + receives Metadata sent by the remote user. + parameter: + metadata:Received Metadata. + */ + virtual void onMetadataReceived(const Metadata &metadata)override; + //set max meta data size. + void SetMaxMetadataSize(int maxSize); + //set send string. + void SetSendSEI(std::string utf8Msg); +private: + int m_maxSize = 1024; + std::string m_sendSEI; + HWND m_hMsgHanlder; +}; + + +class CAgoraMetaDataEventHanlder : public IRtcEngineEventHandler +{ +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override; + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override; + /** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. + */ + virtual void onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) override; +private: + HWND m_hMsgHanlder; +}; + +// CAgoraMetaDataDlg dialog + +class CAgoraMetaDataDlg : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraMetaDataDlg) + +public: + CAgoraMetaDataDlg(CWnd* pParent = nullptr); // standard constructor + virtual ~CAgoraMetaDataDlg(); + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //set control text from config. + void InitCtrlText(); + //render local video from SDK local capture. + void RenderLocalVideo(); + //resume window status. + void ResumeStatus(); + + enum { IDD = IDD_DIALOG_METADATA }; + + //Agora Event handler + afx_msg LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnEIDMetadataReceived(WPARAM wParam, LPARAM lParam); +protected: + virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support + + DECLARE_MESSAGE_MAP() +public: + CStatic m_staChannelName; + CButton m_btnJoinChannel; + CStatic m_staSendSEI; + CEdit m_edtSendSEI; + CEdit m_edtRecvSEI; + CStatic m_staMetaData; + CListBox m_lstInfo; + afx_msg void OnBnClickedButtonJoinchannel(); + virtual BOOL OnInitDialog(); + +private: + IRtcEngine* m_rtcEngine = nullptr; + CAgoraMetaDataEventHanlder m_eventHandler; + + bool m_joinChannel = false; + bool m_initialize = false; + bool m_remoteJoined = false; + CAGVideoWnd m_localVideoWnd; + CAGVideoWnd m_remoteVideoWnd; + + CAgoraMetaDataObserver m_metaDataObserver; +public: + CStatic m_staVideoArea; + CEdit m_edtChannelName; + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + CButton m_btnSendSEI; + afx_msg void OnBnClickedButtonSend(); + afx_msg void OnBnClickedButtonClear(); + virtual BOOL PreTranslateMessage(MSG* pMsg); + CButton m_btnClear; +}; diff --git a/windows/APIExample/APIExample/Advanced/VideoProfile/CAgoraVideoProfileDlg.cpp b/windows/APIExample/APIExample/Advanced/VideoProfile/CAgoraVideoProfileDlg.cpp new file mode 100644 index 000000000..14e07ae93 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/VideoProfile/CAgoraVideoProfileDlg.cpp @@ -0,0 +1,471 @@ +#include "stdafx.h" +#include "APIExample.h" +#include "CAgoraVideoProfileDlg.h" + + + +IMPLEMENT_DYNAMIC(CAgoraVideoProfileDlg, CDialogEx) + +CAgoraVideoProfileDlg::CAgoraVideoProfileDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_VIDEO_PROFILE, pParent) +{ + +} + +CAgoraVideoProfileDlg::~CAgoraVideoProfileDlg() +{ +} + +void CAgoraVideoProfileDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_staVideoArea); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannel); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannel); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_STATIC_VIDEO_WIDTH, m_staWidth); + DDX_Control(pDX, IDC_EDIT_VIDEO_WIDTH, m_edtWidth); + DDX_Control(pDX, IDC_STATIC_VIDEO_HEIGHT, m_staHeight); + DDX_Control(pDX, IDC_EDIT_VIDEO_HEIGHT, m_edtHeight); + DDX_Control(pDX, IDC_STATIC_VIDEO_FPS, m_staFPS); + DDX_Control(pDX, IDC_STATIC_VIDEO_BITRATE, m_staBitrate); + DDX_Control(pDX, IDC_EDIT_VIDEO_BITRATE, m_edtBitrate); + DDX_Control(pDX, IDC_STATIC_VIDEO_DEGRADATION_PREFERENCE, m_staDegradationPre); + DDX_Control(pDX, IDC_COMBO_DEGRADATION_PREFERENCE, m_cmbDegradationPre); + DDX_Control(pDX, IDC_BUTTON_SET_VIDEO_PROFILE, m_btnSetVideoProfile); + DDX_Control(pDX, IDC_STATIC_DETAIL, m_staDetail); + DDX_Control(pDX, IDC_COMBO_FPS, m_cmbFPS); +} + + +BEGIN_MESSAGE_MAP(CAgoraVideoProfileDlg, CDialogEx) + ON_WM_SHOWWINDOW() + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CAgoraVideoProfileDlg::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CAgoraVideoProfileDlg::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CAgoraVideoProfileDlg::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CAgoraVideoProfileDlg::OnEIDUserOffline) + ON_MESSAGE(WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), &CAgoraVideoProfileDlg::OnEIDRemoteVideoStateChanged) + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CAgoraVideoProfileDlg::OnBnClickedButtonJoinchannel) + ON_BN_CLICKED(IDC_BUTTON_SET_VIDEO_PROFILE, &CAgoraVideoProfileDlg::OnBnClickedButtonSetVideoProfile) + ON_LBN_SELCHANGE(IDC_LIST_INFO_BROADCASTING, &CAgoraVideoProfileDlg::OnSelchangeListInfoBroadcasting) +END_MESSAGE_MAP() + + +//init ctrl text. +void CAgoraVideoProfileDlg::InitCtrlText() +{ + m_staChannel.SetWindowText(commonCtrlChannel); + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + m_staDegradationPre.SetWindowText(videoProfileCtrldegradationPreference); + m_staFPS.SetWindowText(videoProfileCtrlFPS); + m_staHeight.SetWindowText(videoProfileCtrlHeight); + m_staWidth.SetWindowText(videoProfileCtrlWidth); + m_staBitrate.SetWindowText(videoProfileCtrlBitrate); + +} + +//Initialize the Agora SDK +bool CAgoraVideoProfileDlg::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount() - 1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //enable video in the engine. + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + return true; +} + +void CAgoraVideoProfileDlg::UnInitAgora() +{ + if (m_rtcEngine) { + if (m_joinChannel) + //leave channel + m_joinChannel = !m_rtcEngine->leaveChannel(); + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} + + +//render local video from SDK local capture. +void CAgoraVideoProfileDlg::RenderLocalVideo() +{ + if (m_rtcEngine) { + //start preview in the engine. + m_rtcEngine->startPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("startPreview")); + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_localVideoWnd.GetSafeHwnd(); + //setup local video in the engine to canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setupLocalVideo")); + } +} + +//resume status. +void CAgoraVideoProfileDlg::ResumeStatus() +{ + InitCtrlText(); + m_staDetail.SetWindowText(_T("")); + m_edtChannel.SetWindowText(_T("")); + m_cmbFPS.SetCurSel(0); + m_edtHeight.SetWindowText(_T("640")); + m_edtWidth.SetWindowText(_T("480")); + m_edtBitrate.SetWindowText(_T("0")); + m_cmbDegradationPre.SetCurSel(0); + m_btnSetVideoProfile.SetWindowText(videoProfileCtrlSetVideoProfile); + + m_lstInfo.ResetContent(); + m_joinChannel = false; + m_initialize = false; + m_setVideo = false; +} + + + +// init dialog +BOOL CAgoraVideoProfileDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + m_localVideoWnd.Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, ID_BASEWND_VIDEO + 100); + RECT rcArea; + m_staVideoArea.GetClientRect(&rcArea); + m_localVideoWnd.MoveWindow(&rcArea); + m_localVideoWnd.ShowWindow(SW_SHOW); + + int nIndex = 0; + m_cmbDegradationPre.InsertString(nIndex++, _T("MAINTAIN_QUALITY")); + m_cmbDegradationPre.InsertString(nIndex++, _T("MAINTAIN_FRAMERATE")); + m_cmbDegradationPre.InsertString(nIndex++, _T("MAINTAIN_BALANCED")); + + nIndex = 0; + m_cmbFPS.InsertString(nIndex++, _T("FRAME_RATE_FPS_1")); + m_cmbFPS.InsertString(nIndex++, _T("FRAME_RATE_FPS_7")); + m_cmbFPS.InsertString(nIndex++, _T("FRAME_RATE_FPS_10")); + m_cmbFPS.InsertString(nIndex++, _T("FRAME_RATE_FPS_15")); + m_cmbFPS.InsertString(nIndex++, _T("FRAME_RATE_FPS_24")); + m_cmbFPS.InsertString(nIndex++, _T("FRAME_RATE_FPS_30")); + m_cmbFPS.InsertString(nIndex++, _T("FRAME_RATE_FPS_60")); + + m_mapFrameRate.insert(std::make_pair(_T("FRAME_RATE_FPS_1"), FRAME_RATE_FPS_1)); + m_mapFrameRate.insert(std::make_pair(_T("FRAME_RATE_FPS_7"), FRAME_RATE_FPS_7)); + m_mapFrameRate.insert(std::make_pair(_T("FRAME_RATE_FPS_10"), FRAME_RATE_FPS_10)); + m_mapFrameRate.insert(std::make_pair(_T("FRAME_RATE_FPS_15"), FRAME_RATE_FPS_15)); + m_mapFrameRate.insert(std::make_pair(_T("FRAME_RATE_FPS_24"), FRAME_RATE_FPS_24)); + m_mapFrameRate.insert(std::make_pair(_T("FRAME_RATE_FPS_30"), FRAME_RATE_FPS_30)); + m_mapFrameRate.insert(std::make_pair(_T("FRAME_RATE_FPS_60"), FRAME_RATE_FPS_60)); + + + + ResumeStatus(); + return TRUE; +} + +// set video profile +void CAgoraVideoProfileDlg::OnBnClickedButtonSetVideoProfile() +{ + VideoEncoderConfiguration config; + CString tmp; + m_edtBitrate.GetWindowText(tmp); + config.bitrate = _ttol(tmp.GetBuffer()); + m_cmbFPS.GetWindowText(tmp); + config.frameRate = m_mapFrameRate[tmp]; + config.degradationPreference = DEGRADATION_PREFERENCE(m_cmbDegradationPre.GetCurSel()); + m_edtWidth.GetWindowText(tmp); + config.dimensions.width = _ttol(tmp.GetBuffer()); + m_edtHeight.GetWindowText(tmp); + config.dimensions.height = _ttol(tmp.GetBuffer()); + m_rtcEngine->setVideoEncoderConfiguration(config); +} + + +// preTranslateMessage handler +BOOL CAgoraVideoProfileDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} + +// show window or hide window. +void CAgoraVideoProfileDlg::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + if (bShow)//bShwo is true ,show window + { + InitCtrlText(); + RenderLocalVideo(); + } + else { + ResumeStatus(); + } + +} + +//join channel handler +void CAgoraVideoProfileDlg::OnBnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + CString strInfo; + if (!m_joinChannel) { + CString strChannelName; + m_edtChannel.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + std::string szChannelId = cs2utf8(strChannelName); + //join channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + strInfo.Format(_T("join channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + } + } + else { + //leave channel in the engine. + if (0 == m_rtcEngine->leaveChannel()) { + strInfo.Format(_T("leave channel %s"), getCurrentTime()); + } + } + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + + + + +//show information in the label +void CAgoraVideoProfileDlg::OnSelchangeListInfoBroadcasting() +{ + int sel = m_lstInfo.GetCurSel(); + if (sel < 0)return; + CString strDetail; + m_lstInfo.GetText(sel, strDetail); + m_staDetail.SetWindowText(strDetail); +} + + +//EID_JOINCHANNEL_SUCCESS message window handler +LRESULT CAgoraVideoProfileDlg::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = true; + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + m_btnJoinChannel.EnableWindow(TRUE); + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_localVideoWnd.SetUID(wParam); + //notify parent window + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); + return 0; +} + +//EID_LEAVEHANNEL_SUCCESS message window handler +LRESULT CAgoraVideoProfileDlg::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + m_joinChannel = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + CString strInfo; + strInfo.Format(_T("leave channel success %s"), getCurrentTime()); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return 0; +} + +//EID_USER_JOINED message window handler +LRESULT CAgoraVideoProfileDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + CString strInfo; + strInfo.Format(_T("%u joined"), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + + return 0; +} + +//EID_USER_OFFLINE message handler. +LRESULT CAgoraVideoProfileDlg::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + m_rtcEngine->setupRemoteVideo(canvas); + CString strInfo; + strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return 0; +} + +//EID_REMOTE_VIDEO_STATE_CHANED message window handler. +LRESULT CAgoraVideoProfileDlg::OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam) +{ + PVideoStateStateChanged stateChanged = (PVideoStateStateChanged)wParam; + if (stateChanged) { + //onRemoteVideoStateChanged + CString strSateInfo; + switch (stateChanged->state) { + case REMOTE_VIDEO_STATE_STARTING: + strSateInfo = _T("REMOTE_VIDEO_STATE_STARTING"); + break; + case REMOTE_VIDEO_STATE_STOPPED: + strSateInfo = _T("strSateInfo"); + break; + case REMOTE_VIDEO_STATE_DECODING: + strSateInfo = _T("REMOTE_VIDEO_STATE_DECODING"); + break; + case REMOTE_VIDEO_STATE_FAILED: + strSateInfo = _T("REMOTE_VIDEO_STATE_FAILED "); + break; + case REMOTE_VIDEO_STATE_FROZEN: + strSateInfo = _T("REMOTE_VIDEO_STATE_FROZEN "); + break; + } + CString strInfo; + strInfo.Format(_T("onRemoteVideoStateChanged: uid=%u, %s"), stateChanged->uid, strSateInfo); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + } + return 0; +} + + + +/* +note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one +parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). +*/ +void CAgoraVideoProfileEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } +} +/* +note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. +parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). +*/ +void CAgoraVideoProfileEventHandler::onUserJoined(uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } +} + +/* +note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. +parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. +*/ +void CAgoraVideoProfileEventHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } +} +/* +note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. +parameters: + stats: Call statistics. +*/ + +void CAgoraVideoProfileEventHandler::onLeaveChannel(const RtcStats& stats) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } +} +/** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. +*/ +void CAgoraVideoProfileEventHandler::onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) +{ + if (m_hMsgHanlder) { + PVideoStateStateChanged stateChanged = new VideoStateStateChanged; + stateChanged->uid = uid; + stateChanged->reason = reason; + stateChanged->state = state; + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_REMOTE_VIDEO_STATE_CHANED), (WPARAM)stateChanged, 0); + } +} \ No newline at end of file diff --git a/windows/APIExample/APIExample/Advanced/VideoProfile/CAgoraVideoProfileDlg.h b/windows/APIExample/APIExample/Advanced/VideoProfile/CAgoraVideoProfileDlg.h new file mode 100644 index 000000000..a7b9ebba2 --- /dev/null +++ b/windows/APIExample/APIExample/Advanced/VideoProfile/CAgoraVideoProfileDlg.h @@ -0,0 +1,153 @@ +#pragma once +#include "AGVideoWnd.h" + + +class CAgoraVideoProfileEventHandler : public IRtcEngineEventHandler +{ +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override; + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override; + /** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. + */ + virtual void onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) override; +private: + HWND m_hMsgHanlder; +}; + + +class CAgoraVideoProfileDlg : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraVideoProfileDlg) + +public: + CAgoraVideoProfileDlg(CWnd* pParent = nullptr); + virtual ~CAgoraVideoProfileDlg(); + + enum { IDD = IDD_DIALOG_VIDEO_PROFILE }; + +public: + + //Initialize the Ctrl Text. + void InitCtrlText(); + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //render local video from SDK local capture. + void RenderLocalVideo(); + //resume window status + void ResumeStatus(); + +private: + bool m_joinChannel = false; + bool m_initialize = false; + bool m_setVideo = false; + IRtcEngine* m_rtcEngine = nullptr; + CAGVideoWnd m_localVideoWnd; + CAgoraVideoProfileEventHandler m_eventHandler; + +public: + LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam); + + +protected: + virtual void DoDataExchange(CDataExchange* pDX); + + DECLARE_MESSAGE_MAP() +public: + CStatic m_staVideoArea; + CListBox m_lstInfo; + CStatic m_staChannel; + CEdit m_edtChannel; + CButton m_btnJoinChannel; + CStatic m_staWidth; + CEdit m_edtWidth; + CStatic m_staHeight; + CEdit m_edtHeight; + CStatic m_staFPS; + CStatic m_staBitrate; + CEdit m_edtBitrate; + CStatic m_staDegradationPre; + CComboBox m_cmbDegradationPre; + CButton m_btnSetVideoProfile; + CStatic m_staDetail; + CComboBox m_cmbFPS; + std::map m_mapFrameRate; + + virtual BOOL OnInitDialog(); + virtual BOOL PreTranslateMessage(MSG* pMsg); + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + afx_msg void OnBnClickedButtonJoinchannel(); + afx_msg void OnBnClickedButtonSetVideoProfile(); + afx_msg void OnSelchangeListInfoBroadcasting(); +}; diff --git a/windows/APIExample/APIExample/Basic/LiveBroadcasting/CLiveBroadcastingDlg.cpp b/windows/APIExample/APIExample/Basic/LiveBroadcasting/CLiveBroadcastingDlg.cpp new file mode 100644 index 000000000..e54837137 --- /dev/null +++ b/windows/APIExample/APIExample/Basic/LiveBroadcasting/CLiveBroadcastingDlg.cpp @@ -0,0 +1,498 @@ +// CLiveBroadcastingDlg.cpp : implementation file + +#include "stdafx.h" +#include "APIExample.h" +#include "CLiveBroadcastingDlg.h" + +/* +note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one +parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). +*/ +void CLiveBroadcastingRtcEngineEventHandler::onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_JOINCHANNEL_SUCCESS), (WPARAM)uid, (LPARAM)elapsed); + } +} + +/* +note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. +parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). +*/ +void CLiveBroadcastingRtcEngineEventHandler::onUserJoined(uid_t uid, int elapsed) { + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_JOINED), (WPARAM)uid, (LPARAM)elapsed); + } +} + +/* +note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. +parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. +*/ +void CLiveBroadcastingRtcEngineEventHandler::onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_USER_OFFLINE), (WPARAM)uid, (LPARAM)reason); + } +} + +/* +note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. +parametes: + stats: Call statistics. +*/ +void CLiveBroadcastingRtcEngineEventHandler::onLeaveChannel(const RtcStats& stats) +{ + if (m_hMsgHanlder) { + ::PostMessage(m_hMsgHanlder, WM_MSGID(EID_LEAVE_CHANNEL), 0, 0); + } +} +// CLiveBroadcastingDlg dialog +IMPLEMENT_DYNAMIC(CLiveBroadcastingDlg, CDialogEx) + +CLiveBroadcastingDlg::CLiveBroadcastingDlg(CWnd* pParent /*=nullptr*/) + : CDialogEx(IDD_DIALOG_LIVEBROADCASTING, pParent) +{ + +} + +CLiveBroadcastingDlg::~CLiveBroadcastingDlg() +{ +} + +void CLiveBroadcastingDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialogEx::DoDataExchange(pDX); + DDX_Control(pDX, IDC_COMBO_ROLE, m_cmbRole); + DDX_Control(pDX, IDC_STATIC_ROLE, m_staRole); + DDX_Control(pDX, IDC_EDIT_CHANNELNAME, m_edtChannelName); + DDX_Control(pDX, IDC_BUTTON_JOINCHANNEL, m_btnJoinChannel); + DDX_Control(pDX, IDC_LIST_INFO_BROADCASTING, m_lstInfo); + DDX_Control(pDX, IDC_STATIC_VIDEO, m_videoArea); + DDX_Control(pDX, IDC_COMBO_PERSONS, m_cmbPersons); + DDX_Control(pDX, IDC_STATIC_PERSONS, m_staPersons); + DDX_Control(pDX, IDC_STATIC_CHANNELNAME, m_staChannelName); + DDX_Control(pDX, IDC_STATIC_DETAIL, m_staDetail); +} + + +BEGIN_MESSAGE_MAP(CLiveBroadcastingDlg, CDialogEx) + ON_BN_CLICKED(IDC_BUTTON_JOINCHANNEL, &CLiveBroadcastingDlg::OnBnClickedButtonJoinchannel) + ON_CBN_SELCHANGE(IDC_COMBO_PERSONS, &CLiveBroadcastingDlg::OnSelchangeComboPersons) + ON_CBN_SELCHANGE(IDC_COMBO_ROLE, &CLiveBroadcastingDlg::OnSelchangeComboRole) + ON_MESSAGE(WM_MSGID(EID_JOINCHANNEL_SUCCESS), &CLiveBroadcastingDlg::OnEIDJoinChannelSuccess) + ON_MESSAGE(WM_MSGID(EID_LEAVE_CHANNEL), &CLiveBroadcastingDlg::OnEIDLeaveChannel) + ON_MESSAGE(WM_MSGID(EID_USER_JOINED), &CLiveBroadcastingDlg::OnEIDUserJoined) + ON_MESSAGE(WM_MSGID(EID_USER_OFFLINE), &CLiveBroadcastingDlg::OnEIDUserOffline) + ON_WM_SHOWWINDOW() + ON_LBN_SELCHANGE(IDC_LIST_INFO_BROADCASTING, &CLiveBroadcastingDlg::OnSelchangeListInfoBroadcasting) + ON_STN_CLICKED(IDC_STATIC_VIDEO, &CLiveBroadcastingDlg::OnStnClickedStaticVideo) +END_MESSAGE_MAP() + + +// CLiveBroadcastingDlg message handlers +BOOL CLiveBroadcastingDlg::OnInitDialog() +{ + CDialogEx::OnInitDialog(); + CreateAllVideoWnds(); + // clientRole + int i = 0; + m_cmbRole.InsertString(i++, agoraRoleBroadcaster); + m_cmbRole.InsertString(i++, agoraRoleAudience); + // + i = 0; + m_cmbPersons.InsertString(i++, _T("1V1")); + m_cmbPersons.InsertString(i++, _T("1V3")); + m_cmbPersons.InsertString(i++, _T("1V8")); + m_cmbPersons.InsertString(i++, _T("1V15")); + ResumeStatus(); + return TRUE; +} + + +//set control text from config. +void CLiveBroadcastingDlg::InitCtrlText() +{ + m_staRole.SetWindowText(commonCtrlClientRole); + m_staPersons.SetWindowText(liveCtrlPersons); + m_staChannelName.SetWindowText(commonCtrlChannel); + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); +} + +//create all video window to save m_videoWnds. +void CLiveBroadcastingDlg::CreateAllVideoWnds() +{ + for (int i = 0; i < VIDEO_COUNT; ++i) { + m_videoWnds[i].Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 1, 1), this, IDC_BASEWND_VIDEO + i); + //set window background color. + m_videoWnds[i].SetFaceColor(RGB(0x58, 0x58, 0x58)); + } +} + +//show all video window from m_videoWnds. +void CLiveBroadcastingDlg::ShowVideoWnds() +{ + m_videoArea.ShowWindow(SW_HIDE); + int row = 2; + int col = 2; + m_maxVideoCount = 4; + switch (m_cmbPersons.GetCurSel()) { + case PEOPLE_IN_CHANNEL_2: { + row = 1; + col = 2; + m_maxVideoCount = 2; + } + break; + case PEOPLE_IN_CHANNEL_4: { + int row = 2; + int col = 2; + m_maxVideoCount = 4; + } + break; + case PEOPLE_IN_CHANNEL_9: { + row = 3; + col = 3; + m_maxVideoCount = 9; + } + break; + case PEOPLE_IN_CHANNEL_16: { + row = 4; + col = 4; + m_maxVideoCount = 16; + } + break; + } + + + RECT rcArea; + m_videoArea.GetClientRect( &rcArea); + int space = 1; + + int w = (rcArea.right -rcArea.left - space * (col - 1)) / col; + int h = (rcArea.bottom - rcArea.top - space * (row - 1)) / row; + + for (int r = 0; r < row; r++) { + for (int c = 0; c < col; c++) { + int x = rcArea.left + (w + space) * c; + int y = rcArea.top + (h + space) * r; + int nIndex = r * col + c; + m_videoWnds[nIndex].MoveWindow(x, y, w, h, TRUE); + m_videoWnds[nIndex].ShowWindow(SW_SHOW); + m_videoWnds[nIndex].SetParent(this); + + if (!m_videoWnds[nIndex].IsWindowVisible()) { + m_videoWnds[nIndex].ShowWindow(SW_SHOW); + } + } + } + + for (int i = m_maxVideoCount; i < VIDEO_COUNT; i++) { + m_videoWnds[i].ShowWindow(0); + if (m_videoWnds[i].GetUID() != 0) { + VideoCanvas canvas; + canvas.uid = m_videoWnds[i].GetUID(); + canvas.view = m_videoWnds[i].GetSafeHwnd(); + m_rtcEngine->setupRemoteVideo(canvas); + } + } +} +//Initialize the Agora SDK +bool CLiveBroadcastingDlg::InitAgora() +{ + //create Agora RTC engine + m_rtcEngine = createAgoraRtcEngine(); + if (!m_rtcEngine) { + m_lstInfo.InsertString(m_lstInfo.GetCount()-1, _T("createAgoraRtcEngine failed")); + return false; + } + //set message notify receiver window + m_eventHandler.SetMsgReceiver(m_hWnd); + + RtcEngineContext context; + std::string strAppID = GET_APP_ID; + context.appId = strAppID.c_str(); + context.eventHandler = &m_eventHandler; + //initialize the Agora RTC engine context. + int ret = m_rtcEngine->initialize(context); + if (ret != 0) { + m_initialize = false; + CString strInfo; + strInfo.Format(_T("initialize failed: %d"), ret); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + return false; + } + else + m_initialize = true; + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("initialize success")); + //enable video in the engine. + m_rtcEngine->enableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("enable video")); + //set channel profile in the engine to the CHANNEL_PROFILE_LIVE_BROADCASTING. + m_rtcEngine->setChannelProfile(CHANNEL_PROFILE_LIVE_BROADCASTING); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("live broadcasting")); + ClientRoleOptions role_options; + role_options.audienceLatencyLevel = AUDIENCE_LATENCY_LEVEL_ULTRA_LOW_LATENCY; + //set client role in the engine to the CLIENT_ROLE_BROADCASTER. + m_rtcEngine->setClientRole(CLIENT_ROLE_BROADCASTER, role_options); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("setClientRole broadcaster")); + return true; +} + +//UnInitialize the Agora SDK +void CLiveBroadcastingDlg::UnInitAgora() +{ + if (m_rtcEngine) { + if(m_joinChannel) + //leave channel + m_rtcEngine->leaveChannel(); + //stop preview in the engine. + m_rtcEngine->stopPreview(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("stopPreview")); + //disable video in the engine. + m_rtcEngine->disableVideo(); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("disableVideo")); + //release engine. + m_rtcEngine->release(true); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("release rtc engine")); + m_rtcEngine = NULL; + } +} + +void CLiveBroadcastingDlg::ResumeStatus() +{ + m_lstInfo.ResetContent(); + m_cmbRole.SetCurSel(0); + m_cmbPersons.SetCurSel(0); + ShowVideoWnds(); + InitCtrlText(); + m_btnJoinChannel.EnableWindow(TRUE); + m_cmbRole.EnableWindow(TRUE); + m_edtChannelName.SetWindowText(_T("")); + m_joinChannel = false; + m_initialize = false; +} + +//render local video from SDK local capture. +void CLiveBroadcastingDlg::RenderLocalVideo() +{ + if (m_rtcEngine) { + //start preview in the engine. + m_rtcEngine->startPreview(); + VideoCanvas canvas; + canvas.renderMode = RENDER_MODE_FIT; + canvas.uid = 0; + canvas.view = m_videoWnds[0].GetSafeHwnd(); + //setup local video in the engine to the canvas. + m_rtcEngine->setupLocalVideo(canvas); + m_lstInfo.InsertString(m_lstInfo.GetCount(), _T("render local video")); + } +} + + +void CLiveBroadcastingDlg::OnSelchangeComboPersons() +{ + int index = m_cmbPersons.GetCurSel(); + ShowVideoWnds(); +} + + +void CLiveBroadcastingDlg::OnSelchangeComboRole() +{ + if (m_rtcEngine) { + m_rtcEngine->setClientRole(CLIENT_ROLE_TYPE(m_cmbRole.GetCurSel() + 1)); + + m_lstInfo.InsertString(m_lstInfo.GetCount(), m_cmbRole.GetCurSel() == 0 ? _T("setClientRole broadcaster"): _T("setClientRole Audience")); + } +} + +void CLiveBroadcastingDlg::OnBnClickedButtonJoinchannel() +{ + if (!m_rtcEngine || !m_initialize) + return; + CString strInfo; + if (!m_joinChannel) { + CString strChannelName; + m_edtChannelName.GetWindowText(strChannelName); + if (strChannelName.IsEmpty()) { + AfxMessageBox(_T("Fill channel name first")); + return; + } + std::string szChannelId = cs2utf8(strChannelName); + //join channel in the engine. + if (0 == m_rtcEngine->joinChannel(APP_TOKEN, szChannelId.c_str(), "", 0)) { + strInfo.Format(_T("join channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + } + } + else { + if (0 == m_rtcEngine->leaveChannel()) { + strInfo.Format(_T("leave channel %s"), getCurrentTime()); + m_btnJoinChannel.EnableWindow(FALSE); + } + } + + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); +} + + + +void CLiveBroadcastingDlg::OnShowWindow(BOOL bShow, UINT nStatus) +{ + CDialogEx::OnShowWindow(bShow, nStatus); + + if (bShow) { + RenderLocalVideo(); + } + else + { + ResumeStatus(); + } +} + +LRESULT CLiveBroadcastingDlg::OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam) +{ + m_btnJoinChannel.EnableWindow(TRUE); + m_joinChannel = true; + m_btnJoinChannel.SetWindowText(commonCtrlLeaveChannel); + + CString strInfo; + strInfo.Format(_T("%s:join success, uid=%u"), getCurrentTime(), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + + m_videoWnds[0].SetUID(wParam); + m_lstUids.push_back(wParam); + + //notify parent window + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), TRUE, 0); + return 0; +} + +LRESULT CLiveBroadcastingDlg::OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam) +{ + m_btnJoinChannel.EnableWindow(TRUE); + m_joinChannel = false; + m_btnJoinChannel.SetWindowText(commonCtrlJoinChannel); + + CString strInfo; + strInfo.Format(_T("leave channel success %s"), getCurrentTime()); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + m_lstUids.clear(); + for (int i = 0; i < m_maxVideoCount; i++) { + m_videoWnds[i].SetUID(0); + } + + //notify parent window + ::PostMessage(GetParent()->GetSafeHwnd(), WM_MSGID(EID_JOINCHANNEL_SUCCESS), FALSE, 0); + return 0; +} + +LRESULT CLiveBroadcastingDlg::OnEIDUserJoined(WPARAM wParam, LPARAM lParam) +{ + if (m_lstUids.size() == m_maxVideoCount) + return 0; + CString strInfo; + strInfo.Format(_T("%u joined"), wParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + + m_lstUids.push_back(wParam); + for (int i = 0; i < m_maxVideoCount; i++) { + if (m_videoWnds[i].GetUID() == 0) { + VideoCanvas canvas; + canvas.uid = wParam; + canvas.view = m_videoWnds[i].GetSafeHwnd(); + canvas.renderMode = RENDER_MODE_FIT; + m_videoWnds[i].SetUID(wParam); + //setup remote video in engine to the canvas. + m_rtcEngine->setupRemoteVideo(canvas); + break; + } + } + return 0; +} + +LRESULT CLiveBroadcastingDlg::OnEIDUserOffline(WPARAM wParam, LPARAM lParam) +{ + uid_t remoteUid = (uid_t)wParam; + VideoCanvas canvas; + canvas.uid = remoteUid; + canvas.view = NULL; + m_rtcEngine->setupRemoteVideo(canvas); + CString strInfo; + strInfo.Format(_T("%u offline, reason:%d"), remoteUid, lParam); + m_lstInfo.InsertString(m_lstInfo.GetCount(), strInfo); + + for (int i = 0; i < m_maxVideoCount; i++){ + if (m_videoWnds[i].GetUID() == remoteUid) { + m_videoWnds[i].SetUID(0); + m_videoWnds[i].Invalidate(); + break; + } + } + + for (auto iter = m_lstUids.begin(); + iter != m_lstUids.end(); iter++){ + if (*iter == remoteUid) { + m_lstUids.erase(iter); + break; + } + } + return 0; +} + + +void CLiveBroadcastingDlg::OnSelchangeListInfoBroadcasting() +{ + int sel = m_lstInfo.GetCurSel(); + if (sel < 0)return; + CString strDetail; + m_lstInfo.GetText(sel, strDetail); + m_staDetail.SetWindowText(strDetail); +} + + +void CLiveBroadcastingDlg::OnStnClickedStaticVideo() +{ +} + + +BOOL CLiveBroadcastingDlg::PreTranslateMessage(MSG* pMsg) +{ + if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) { + return TRUE; + } + return CDialogEx::PreTranslateMessage(pMsg); +} diff --git a/windows/APIExample/APIExample/Basic/LiveBroadcasting/CLiveBroadcastingDlg.h b/windows/APIExample/APIExample/Basic/LiveBroadcasting/CLiveBroadcastingDlg.h new file mode 100644 index 000000000..1d0996f28 --- /dev/null +++ b/windows/APIExample/APIExample/Basic/LiveBroadcasting/CLiveBroadcastingDlg.h @@ -0,0 +1,150 @@ +#pragma once +#include "AGVideoWnd.h" +#include +// CLiveBroadcastingDlg dialog + +#define VIDEO_COUNT 36 +#define IDC_BASEWND_VIDEO 113 + +class CLiveBroadcastingRtcEngineEventHandler + : public IRtcEngineEventHandler +{ +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override; + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parametes: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override; +private: + HWND m_hMsgHanlder; +}; + +class CLiveBroadcastingDlg : public CDialogEx +{ + DECLARE_DYNAMIC(CLiveBroadcastingDlg) + +public: + CLiveBroadcastingDlg(CWnd* pParent = nullptr); // standard constructor + virtual ~CLiveBroadcastingDlg(); + + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + + //resume window status + void ResumeStatus(); + +// Dialog Data + enum { IDD = IDD_DIALOG_LIVEBROADCASTING }; + //The number of people supported within the channel + enum PEOPLE_IN_CHANNEL_TYPE { + PEOPLE_IN_CHANNEL_2 = 0, + PEOPLE_IN_CHANNEL_4, + PEOPLE_IN_CHANNEL_9, + PEOPLE_IN_CHANNEL_16 + }; +protected: + virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support + + DECLARE_MESSAGE_MAP() +public: + afx_msg void OnSelchangeComboPersons(); + afx_msg void OnSelchangeComboRole(); + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + //Agora Event handler + afx_msg LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + +private: + //set control text from config. + void InitCtrlText(); + //create all video window to save m_videoWnds. + void CreateAllVideoWnds(); + //show all video window from m_videoWnds. + void ShowVideoWnds(); + //render local video from SDK local capture. + void RenderLocalVideo(); + + + IRtcEngine* m_rtcEngine = nullptr; + CLiveBroadcastingRtcEngineEventHandler m_eventHandler; + bool m_joinChannel = false; + bool m_initialize = false; + //video wnd + CAGVideoWnd m_videoWnds[VIDEO_COUNT]; + int m_maxVideoCount = 4; + std::list m_lstUids; + +public: + virtual BOOL OnInitDialog(); + afx_msg void OnBnClickedButtonJoinchannel(); + afx_msg void OnSelchangeListInfoBroadcasting(); + afx_msg void OnStnClickedStaticVideo(); + + CComboBox m_cmbRole; + CStatic m_staRole; + CComboBox m_cmbPersons; + CEdit m_edtChannelName; + CButton m_btnJoinChannel; + CListBox m_lstInfo; + CStatic m_videoArea; + CStatic m_staPersons; + CStatic m_staChannelName; + CStatic m_staDetail; + virtual BOOL PreTranslateMessage(MSG* pMsg); +}; diff --git a/windows/APIExample/APIExample/CAgoraEffectDlg.h b/windows/APIExample/APIExample/CAgoraEffectDlg.h new file mode 100644 index 000000000..e7ecc2ecf --- /dev/null +++ b/windows/APIExample/APIExample/CAgoraEffectDlg.h @@ -0,0 +1,167 @@ +#pragma once + +#include "AGVideoWnd.h" +class CAudioEffectEventHandler : public IRtcEngineEventHandler +{ +public: + //set the message notify window handler + void SetMsgReceiver(HWND hWnd) { m_hMsgHanlder = hWnd; } + + /* + note: + Join the channel callback.This callback method indicates that the client + successfully joined the specified channel.Channel ids are assigned based + on the channel name specified in the joinChannel. If IRtcEngine::joinChannel + is called without a user ID specified. The server will automatically assign one + parameters: + channel:channel name. + uid: user ID.If the UID is specified in the joinChannel, that ID is returned here; + Otherwise, use the ID automatically assigned by the Agora server. + elapsed: The Time from the joinChannel until this event occurred (ms). + */ + virtual void onJoinChannelSuccess(const char* channel, uid_t uid, int elapsed) override; + /* + note: + In the live broadcast scene, each anchor can receive the callback + of the new anchor joining the channel, and can obtain the uID of the anchor. + Viewers also receive a callback when a new anchor joins the channel and + get the anchor's UID.When the Web side joins the live channel, the SDK will + default to the Web side as long as there is a push stream on the + Web side and trigger the callback. + parameters: + uid: remote user/anchor ID for newly added channel. + elapsed: The joinChannel is called from the local user to the delay triggered + by the callback(ms). + */ + virtual void onUserJoined(uid_t uid, int elapsed) override; + /* + note: + Remote user (communication scenario)/anchor (live scenario) is called back from + the current channel.A remote user/anchor has left the channel (or dropped the line). + There are two reasons for users to leave the channel, namely normal departure and + time-out:When leaving normally, the remote user/anchor will send a message like + "goodbye". After receiving this message, determine if the user left the channel. + The basis of timeout dropout is that within a certain period of time + (live broadcast scene has a slight delay), if the user does not receive any + packet from the other side, it will be judged as the other side dropout. + False positives are possible when the network is poor. We recommend using the + Agora Real-time messaging SDK for reliable drop detection. + parameters: + uid: The user ID of an offline user or anchor. + reason:Offline reason: USER_OFFLINE_REASON_TYPE. + */ + virtual void onUserOffline(uid_t uid, USER_OFFLINE_REASON_TYPE reason) override; + /* + note: + When the App calls the leaveChannel method, the SDK indicates that the App + has successfully left the channel. In this callback method, the App can get + the total call time, the data traffic sent and received by THE SDK and other + information. The App obtains the call duration and data statistics received + or sent by the SDK through this callback. + parameters: + stats: Call statistics. + */ + virtual void onLeaveChannel(const RtcStats& stats) override; + /** + Occurs when the remote video state changes. + @note This callback does not work properly when the number of users (in the Communication profile) or broadcasters (in the Live-broadcast profile) in the channel exceeds 17. + + @param uid ID of the remote user whose video state changes. + @param state State of the remote video. See #REMOTE_VIDEO_STATE. + @param reason The reason of the remote video state change. See + #REMOTE_VIDEO_STATE_REASON. + @param elapsed Time elapsed (ms) from the local user calling the + \ref agora::rtc::IRtcEngine::joinChannel "joinChannel" method until the + SDK triggers this callback. + */ + virtual void onRemoteVideoStateChanged(uid_t uid, REMOTE_VIDEO_STATE state, REMOTE_VIDEO_STATE_REASON reason, int elapsed) override; +private: + HWND m_hMsgHanlder; +}; + + +class CAgoraEffectDlg : public CDialogEx +{ + DECLARE_DYNAMIC(CAgoraEffectDlg) + +public: + CAgoraEffectDlg(CWnd* pParent = nullptr); + virtual ~CAgoraEffectDlg(); + + enum { IDD = IDD_DIALOG_AUDIO_EFFECT }; +public: + //Initialize the Ctrl Text. + void InitCtrlText(); + //Initialize the Agora SDK + bool InitAgora(); + //UnInitialize the Agora SDK + void UnInitAgora(); + //render local video from SDK local capture. + void RenderLocalVideo(); + //resume window status + void ResumeStatus(); + +private: + bool m_joinChannel = false; + bool m_initialize = false; + bool m_audioMixing = false; + IRtcEngine* m_rtcEngine = nullptr; + CAGVideoWnd m_localVideoWnd; + CAudioEffectEventHandler m_eventHandler; + +protected: + virtual void DoDataExchange(CDataExchange* pDX); + DECLARE_MESSAGE_MAP() + LRESULT OnEIDJoinChannelSuccess(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDLeaveChannel(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserJoined(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDUserOffline(WPARAM wParam, LPARAM lParam); + LRESULT OnEIDRemoteVideoStateChanged(WPARAM wParam, LPARAM lParam); +public: + CStatic m_staVideoArea; + CListBox m_lstInfo; + CStatic m_staChannel; + CEdit m_edtChannel; + CButton m_btnJoinChannel; + CStatic m_staEffectPath; + CEdit m_edtEffectPath; + CButton m_btnAddEffect; + CButton m_btnPreLoad; + CButton m_btnUnload; + CButton m_btnRemove; + CButton m_btnPause; + CButton m_btnResume; + CStatic m_staDetails; + CStatic m_staLoops; + CEdit m_edtLoops; + CStatic m_staGain; + CEdit m_edtGain; + CSpinButtonCtrl m_spinGain; + CStatic m_staPitch; + CEdit m_edtPitch; + CSpinButtonCtrl m_spinPitch; + CStatic m_staPan; + CComboBox m_cmbPan; + CButton m_chkPublish; + CButton m_btnPlay; + CButton m_btnPauseAll; + CButton m_btnStopAll; + afx_msg void OnBnClickedButtonJoinchannel(); + afx_msg void OnBnClickedButtonAddEffect(); + afx_msg void OnBnClickedButtonPreload(); + afx_msg void OnBnClickedButtonUnloadEffect(); + afx_msg void OnBnClickedButtonRemove(); + afx_msg void OnBnClickedButtonPauseEffect(); + afx_msg void OnBnClickedButtonResumeEffect(); + afx_msg void OnBnClickedButtonPlayEffect(); + afx_msg void OnBnClickedButtonPauseAllEffect(); + afx_msg void OnBnClickedButtonStopAllEffect2(); + afx_msg void OnDeltaposSpinGain(NMHDR *pNMHDR, LRESULT *pResult); + afx_msg void OnDeltaposSpinPitch(NMHDR *pNMHDR, LRESULT *pResult); + afx_msg void OnSelchangeListInfoBroadcasting(); + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + virtual BOOL OnInitDialog(); + virtual BOOL PreTranslateMessage(MSG* pMsg); + CButton m_btnStopEffect; + afx_msg void OnBnClickedButtonStopEffect(); +}; diff --git a/windows/APIExample/APIExample/CConfig.cpp b/windows/APIExample/APIExample/CConfig.cpp new file mode 100644 index 000000000..63e9dcc08 --- /dev/null +++ b/windows/APIExample/APIExample/CConfig.cpp @@ -0,0 +1,82 @@ +#include "stdafx.h" +#include "CConfig.h" +CConfig* CConfig::GetInstance() +{ + static CConfig config; + return &config; +} + +CConfig::CConfig() +{ + ::GetModuleFileName(NULL, m_szZhConfigFile, MAX_PATH); + LPTSTR lpLastSlash = _tcsrchr(m_szZhConfigFile, _T('\\')) + 1; + _tcscpy_s(lpLastSlash, MAX_PATH, _T("zh-cn.ini")); + + if (::GetFileAttributes(m_szZhConfigFile) == INVALID_FILE_ATTRIBUTES) { + HANDLE hFile = ::CreateFile(m_szZhConfigFile, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + ::CloseHandle(hFile); + } + + ::GetModuleFileName(NULL, m_szEnConfigFile, MAX_PATH); + LPTSTR lpLastSlashEn = _tcsrchr(m_szEnConfigFile, _T('\\')) + 1; + _tcscpy_s(lpLastSlashEn, MAX_PATH, _T("en.ini")); + + if (::GetFileAttributes(m_szEnConfigFile) == INVALID_FILE_ATTRIBUTES) { + HANDLE hFile = ::CreateFile(m_szEnConfigFile, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + ::CloseHandle(hFile); + } + LCID lcid = GetUserDefaultLCID();//LCID https://www.science.co.il/language/Locale-codes.php + if (lcid == 2052) {//chinese + m_bChinese = true; + } + +} + + +CConfig::~CConfig() +{ +} + + +CString CConfig::GetStringValue(CString key) +{ + CString strValue = _T(""); + if (m_bChinese) + ::GetPrivateProfileString(_T("General"), key, _T("Unknown"), strValue.GetBuffer(MAX_PATH), MAX_PATH, m_szZhConfigFile); + else + ::GetPrivateProfileString(_T("General"), key, _T("Unknown"), strValue.GetBuffer(MAX_PATH), MAX_PATH, m_szEnConfigFile); + return strValue; +} + + +CString CConfig::GetAPP_ID() +{ + CString strAppID(APP_ID); + if (strAppID.Compare(_T(""))) + return strAppID; + TCHAR szFilePath[MAX_PATH]; + ::GetModuleFileName(NULL, szFilePath, MAX_PATH); + LPTSTR lpLastSlash = _tcsrchr(szFilePath, _T('\\')); + + if (lpLastSlash == NULL) + return strAppID; + + SIZE_T nNameLen = MAX_PATH - (lpLastSlash - szFilePath + 1); + _tcscpy_s(lpLastSlash + 1, nNameLen, _T("AppID.ini")); + + if (!PathFileExists(szFilePath)) { + HANDLE handle = CreateFile(szFilePath, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_NEW, 0, NULL); + CloseHandle(handle); + } + + TCHAR szAppid[MAX_PATH] = { 0 }; + ::GetPrivateProfileString(_T("AppID"), _T("AppID"), NULL, szAppid, MAX_PATH, szFilePath); + if (_tcslen(szAppid) == 0) { + ::WritePrivateProfileString(_T("AppID"), _T("AppID"), _T(""), szFilePath); + ::ShellExecute(NULL, _T("open"), szFilePath, NULL, NULL, SW_MAXIMIZE); + } + + strAppID = szAppid; + + return strAppID; +} diff --git a/windows/APIExample/APIExample/CConfig.h b/windows/APIExample/APIExample/CConfig.h new file mode 100644 index 000000000..b9b91a268 --- /dev/null +++ b/windows/APIExample/APIExample/CConfig.h @@ -0,0 +1,25 @@ +#pragma once +#include +#include +#define Str(key) CConfig::GetInstance()->GetStringValue(key) +#define GET_APP_ID cs2utf8(CConfig::GetInstance()->GetAPP_ID()) + + +class CConfig +{ +public: + CConfig(); + ~CConfig(); + + static CConfig* GetInstance(); + CString GetStringValue(CString key); + CString GetAPP_ID(); +private: + + TCHAR m_szZhConfigFile[MAX_PATH]; + TCHAR m_szEnConfigFile[MAX_PATH]; + bool m_bChinese = false; + + int hashSize = 1000; +}; + diff --git a/windows/APIExample/APIExample/CSceneDialog.cpp b/windows/APIExample/APIExample/CSceneDialog.cpp new file mode 100644 index 000000000..2684d32a4 --- /dev/null +++ b/windows/APIExample/APIExample/CSceneDialog.cpp @@ -0,0 +1,17 @@ +#include "stdafx.h" +#include "CSceneDialog.h" + +IMPLEMENT_DYNAMIC(CSceneDialog, CDialogEx) + +CSceneDialog::CSceneDialog(UINT nIDTemplate, CWnd *pParent) + :CDialogEx(nIDTemplate, pParent) +{ +} + + +CSceneDialog::~CSceneDialog() +{ +} + +BEGIN_MESSAGE_MAP(CSceneDialog, CDialogEx) +END_MESSAGE_MAP() \ No newline at end of file diff --git a/windows/APIExample/APIExample/CSceneDialog.h b/windows/APIExample/APIExample/CSceneDialog.h new file mode 100644 index 000000000..a423d5225 --- /dev/null +++ b/windows/APIExample/APIExample/CSceneDialog.h @@ -0,0 +1,17 @@ +#pragma once +#include +class CSceneDialog : + public CDialogEx +{ + DECLARE_DYNAMIC(CSceneDialog) +public: + + CSceneDialog(UINT nIDTemplate, CWnd *pParent = NULL); + ~CSceneDialog(); + + virtual bool InitAgora() = 0; + virtual void UnInitAgora() = 0; +protected: + DECLARE_MESSAGE_MAP() +}; + diff --git a/windows/APIExample/APIExample/DirectShow/AGDShowAudioCapture.cpp b/windows/APIExample/APIExample/DirectShow/AGDShowAudioCapture.cpp new file mode 100644 index 000000000..9e5d37e6f --- /dev/null +++ b/windows/APIExample/APIExample/DirectShow/AGDShowAudioCapture.cpp @@ -0,0 +1,710 @@ +#include "AGDShowAudioCapture.h" +#include "DShowHelper.h" +#include "CircleBuffer.hpp" +#include + + + + +CAGDShowAudioCapture::CAGDShowAudioCapture() + : m_ptrGraphBuilder(NULL) + , m_ptrCaptureGraphBuilder2(NULL) + , m_nCapSelected(-1) +{ + memset(m_szActiveDeviceID, 0, MAX_PATH*sizeof(TCHAR)); + + filterSourceName = _T("Audio Filter"); + filterCaptureName = _T("Audio Capture Filter"); +} + + +CAGDShowAudioCapture::~CAGDShowAudioCapture() +{ + Close(); +} + +BOOL CAGDShowAudioCapture::Create() +{ + HRESULT hResult = S_OK; + BOOL bRet = FALSE; + do { + hResult = m_ptrGraphBuilder.CoCreateInstance(CLSID_FilterGraph); + ATLASSERT(SUCCEEDED(hResult)); + if (hResult != S_OK) + break; + hResult = m_ptrCaptureGraphBuilder2.CoCreateInstance(CLSID_CaptureGraphBuilder2); + ATLASSERT(SUCCEEDED(hResult)); + if (hResult != S_OK) + break; + + hResult = m_ptrCaptureGraphBuilder2->SetFiltergraph(m_ptrGraphBuilder); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + break; + + if (S_OK != m_ptrGraphBuilder->QueryInterface(IID_IMediaControl, (void**)&control)) + break; + bRet = TRUE; + + } while (FALSE); + + return bRet; +} + +void CAGDShowAudioCapture::Close() +{ + CComPtr filterEnum = nullptr; + HRESULT hr; + + if (!m_ptrGraphBuilder) + return; + + hr = m_ptrGraphBuilder->EnumFilters(&filterEnum); + if (FAILED(hr)) + return; + + if (capturePin) + capturePin.Release(); + + if (filterPin) + filterPin.Release(); + + CComPtr filter = nullptr; + while (filterEnum->Next(1, &filter, nullptr) == S_OK) { + m_ptrGraphBuilder->RemoveFilter(filter); + filterEnum->Reset(); + filter.Release(); + } + + control.Release(); + m_ptrGraphBuilder.Release(); + m_ptrCaptureGraphBuilder2.Release(); +} + +BOOL CAGDShowAudioCapture::EnumDeviceList() +{ + HRESULT hResult = S_OK; + + CComVariant var; + WCHAR *wszDevicePath = NULL; + + CComPtr ptrCreateDevEnum = NULL; + CComPtr ptrEnumMoniker = NULL; + CComPtr ptrMoniker = NULL; + + AGORA_DEVICE_INFO agDeviceInfo; + + hResult = ptrCreateDevEnum.CoCreateInstance(CLSID_SystemDeviceEnum); + if (FAILED(hResult)) + return FALSE; + + hResult = ptrCreateDevEnum->CreateClassEnumerator(CLSID_AudioInputDeviceCategory, &ptrEnumMoniker, 0); + if (hResult != S_OK) + return FALSE; + + m_listDeviceInfo.RemoveAll(); + + do { + ptrMoniker = NULL; + hResult = ptrEnumMoniker->Next(1, &ptrMoniker, NULL); + if (hResult != S_OK) + break; + + IBaseFilter* filter; + if (SUCCEEDED(ptrMoniker->BindToObject(NULL, 0, IID_IBaseFilter, + (void**)&filter))) { + CComPtr ptrPropertyBag = NULL; + hResult = ptrMoniker->BindToStorage(NULL, NULL, IID_IPropertyBag, (void**)(&ptrPropertyBag)); + if (hResult != S_OK) + break; + + memset(&agDeviceInfo, 0, sizeof(AGORA_DEVICE_INFO)); + + var.Clear(); + hResult = ptrPropertyBag->Read(L"FriendlyName", &var, NULL); + if (SUCCEEDED(hResult)) { +#ifdef UNICODE + _tcscpy_s(agDeviceInfo.szDeviceName, var.bstrVal); +#else + ::WideCharToMultiByte(CP_ACP, 0, var.bstrVal, -1, agDeviceInfo.szDeviceName, MAX_PATH, NULL, NULL); +#endif + } + + hResult = ptrMoniker->GetDisplayName(NULL, NULL, &wszDevicePath); + if (SUCCEEDED(hResult)) { +#ifdef UNICODE + _tcscpy_s(agDeviceInfo.szDevicePath, wszDevicePath); +#else + ::WideCharToMultiByte(CP_ACP, 0, wszDevicePath, -1, agDeviceInfo.szDevicePath, MAX_PATH, NULL, NULL); +#endif + ::CoTaskMemFree(wszDevicePath); + } + + m_listDeviceInfo.AddTail(agDeviceInfo); + } + } while (TRUE); + + return TRUE; +} + + BOOL CAGDShowAudioCapture::GetDeviceInfo(int nIndex, LPAGORA_DEVICE_INFO lpDeviceInfo) +{ + ATLASSERT(lpDeviceInfo != NULL); + ATLASSERT(nIndex >= 0 && nIndex < static_cast(m_listDeviceInfo.GetCount())); + + POSITION pos = m_listDeviceInfo.FindIndex(nIndex); + if (pos == NULL) + return FALSE; + + AGORA_DEVICE_INFO &agDeviceInfo = m_listDeviceInfo.GetAt(pos); + memcpy(lpDeviceInfo, &agDeviceInfo, sizeof(AGORA_DEVICE_INFO)); + + return TRUE; +} + +BOOL CAGDShowAudioCapture::OpenDevice(int nIndex) +{ + ATLASSERT(nIndex >= 0 && nIndex < static_cast(m_listDeviceInfo.GetCount())); + + m_nCapSelected = -1; + POSITION pos = m_listDeviceInfo.FindIndex(nIndex); + if (pos == NULL) + return FALSE; + + LPCTSTR lpDevicePath = m_listDeviceInfo.GetAt(pos).szDevicePath; + + return OpenDevice(lpDevicePath); +} + +BOOL CAGDShowAudioCapture::OpenDevice(LPCTSTR lpDevicePath) +{ + ULONG ulEaten = 0; + HRESULT hResult = S_OK; + + CComPtr lpBindCtx = NULL; + CComPtr ptrMoniker = NULL; + CComPtr ptrSourceFilter = NULL; + + m_nCapSelected = -1; + + ATLASSERT(_tcslen(m_szActiveDeviceID) == 0); + + hResult = ::CreateBindCtx(0, &lpBindCtx); + if (hResult != S_OK) + return FALSE; + +#ifdef UNICODE + hResult = ::MkParseDisplayName(lpBindCtx, lpDevicePath, &ulEaten, &ptrMoniker); +#else + WCHAR wszDeviceID[128]; + memset(wszDeviceID, 0, 128 * sizeof(WCHAR)); + ::MultiByteToWideChar(CP_ACP, 0, lpDevicePath, -1, wszDeviceID, 128); + hResult = ::MkParseDisplayName(lpBindCtx, wszDeviceID, &ulEaten, &ptrMoniker); +#endif + + if (hResult != S_OK) + return FALSE; + + hResult = ptrMoniker->BindToObject(0, 0, IID_IBaseFilter, (void **)&ptrSourceFilter); + ATLASSERT(SUCCEEDED(hResult)); + if (hResult != S_OK) + return FALSE; + + hResult = m_ptrGraphBuilder->AddFilter(ptrSourceFilter, filterSourceName); + ATLASSERT(SUCCEEDED(hResult)); + if (hResult != S_OK) + return FALSE; + + _tcscpy_s(m_szActiveDeviceID, MAX_PATH, lpDevicePath); + SelectMediaCap(0); + + return TRUE; +} + +BOOL CAGDShowAudioCapture::GetCurrentDevice(LPTSTR lpDevicePath, SIZE_T *nDevicePathLen) +{ + int nDeviceLen = _tcslen(m_szActiveDeviceID); + if (nDeviceLen >= static_cast(*nDevicePathLen)) { + *nDevicePathLen = nDeviceLen + 1; + return FALSE; + } + + if (nDeviceLen == 0) + return FALSE; + + _tcscpy_s(lpDevicePath, *nDevicePathLen, m_szActiveDeviceID); + *nDevicePathLen = nDeviceLen + 1; + + return TRUE; +} + +void CAGDShowAudioCapture::CloseDevice() +{ + CComPtr ptrSourceFilter = NULL; + + HRESULT hResult = m_ptrGraphBuilder->FindFilterByName(filterSourceName, &ptrSourceFilter); + + if (SUCCEEDED(hResult)) { + m_ptrGraphBuilder->RemoveFilter(ptrSourceFilter); + m_nCapSelected = -1; + } + + memset(m_szActiveDeviceID, 0, MAX_PATH*sizeof(TCHAR)); +} + +int CAGDShowAudioCapture::GetMediaCapCount() +{ + int nCount = 0; + int nSize = 0; + HRESULT hResult = S_OK; + + CComPtr ptrCaptureFilter = NULL; + CComPtr ptrStreamConfig = NULL; + + hResult = m_ptrGraphBuilder->FindFilterByName(filterSourceName, &ptrCaptureFilter); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return 0; + + hResult = m_ptrCaptureGraphBuilder2->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Audio, ptrCaptureFilter, IID_IAMStreamConfig, (void**)&ptrStreamConfig); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return 0; + + hResult = ptrStreamConfig->GetNumberOfCapabilities(&nCount, &nSize); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return 0; + + return nCount; +} + +BOOL CAGDShowAudioCapture::GetMediaCap(int nIndex, AM_MEDIA_TYPE **ppMediaType, LPVOID lpMediaStreamConfigCaps, SIZE_T nSize) +{ + int nCount = 0; + int nCapSize = 0; + HRESULT hResult = S_OK; + + CComPtr ptrCaptureFilter = NULL; + CComPtr ptrStreamConfig = NULL; + + hResult = m_ptrGraphBuilder->FindFilterByName(filterSourceName, &ptrCaptureFilter); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return FALSE; + + hResult = m_ptrCaptureGraphBuilder2->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Audio, ptrCaptureFilter, IID_IAMStreamConfig, (void**)&ptrStreamConfig); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return FALSE; + + hResult = ptrStreamConfig->GetNumberOfCapabilities(&nCount, &nCapSize); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return FALSE; + + ATLASSERT(nCapSize <= static_cast(nSize)); + if (nCapSize > static_cast(nSize)) + return FALSE; + + hResult = ptrStreamConfig->GetStreamCaps(nIndex, ppMediaType, reinterpret_cast(lpMediaStreamConfigCaps)); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return FALSE; + + return TRUE; +} + +BOOL CAGDShowAudioCapture::GetAudioCap(int nIndex, WAVEFORMATEX *lpWaveInfo) +{ + int nCount = 0; + int nSize = 0; + + AM_MEDIA_TYPE *lpAMMediaType = NULL; + AUDIO_STREAM_CONFIG_CAPS audioStreamCfgCaps; + + BOOL bSuccess = GetMediaCap(nIndex, &lpAMMediaType, &audioStreamCfgCaps, sizeof(AUDIO_STREAM_CONFIG_CAPS)); + + if (bSuccess && lpAMMediaType->formattype == FORMAT_WaveFormatEx) { + WAVEFORMATEX* pWavInfo = reinterpret_cast(lpAMMediaType->pbFormat); + memcpy_s(lpWaveInfo, sizeof(WAVEFORMATEX), pWavInfo, sizeof(WAVEFORMATEX)); + } + else + bSuccess = FALSE; + + CDShowHelper::FreeMediaType(lpAMMediaType); + + return bSuccess; +} + +BOOL CAGDShowAudioCapture::GetCurrentAudioCap(WAVEFORMATEX *lpWaveInfo) +{ + BOOL bSuccess = FALSE; + HRESULT hResult = S_OK; + AM_MEDIA_TYPE *lpAMMediaType = NULL; + + CComPtr ptrCaptureFilter = NULL; + CComPtr ptrStreamConfig = NULL; + + hResult = m_ptrGraphBuilder->FindFilterByName(filterSourceName, &ptrCaptureFilter); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return FALSE; + + hResult = m_ptrCaptureGraphBuilder2->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Audio, ptrCaptureFilter, IID_IAMStreamConfig, (void**)&ptrStreamConfig); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return FALSE; + + hResult = ptrStreamConfig->GetFormat(&lpAMMediaType); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return FALSE; + + if (lpAMMediaType->formattype == FORMAT_WaveFormatEx) { + WAVEFORMATEX* pWavInfo = reinterpret_cast(lpAMMediaType->pbFormat); + memcpy_s(lpWaveInfo, sizeof(WAVEFORMATEX), pWavInfo, sizeof(WAVEFORMATEX)); + + bSuccess = TRUE; + } + else + bSuccess = FALSE; + + CDShowHelper::FreeMediaType(lpAMMediaType); + + return bSuccess; +} + +BOOL CAGDShowAudioCapture::SelectMediaCap(int nIndex) +{ + int nCount = 0; + int nSize = 0; + HRESULT hResult = S_OK; + + AM_MEDIA_TYPE *lpMediaType = NULL; + + CComPtr ptrCaptureFilter = NULL; + CComPtr ptrStreamConfig = NULL; + + hResult = m_ptrGraphBuilder->FindFilterByName(filterSourceName, &ptrCaptureFilter); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return FALSE; + + hResult = m_ptrCaptureGraphBuilder2->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Audio, ptrCaptureFilter, IID_IAMStreamConfig, (void**)&ptrStreamConfig); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return FALSE; + + hResult = ptrStreamConfig->GetNumberOfCapabilities(&nCount, &nSize); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return FALSE; + + ATLASSERT(nIndex >= 0 && nIndex < nCount); + if (nIndex < 0 || nIndex >= nCount) + nIndex = 0; + + ATLASSERT(nSize <= sizeof(AUDIO_STREAM_CONFIG_CAPS)); + + do { + hResult = ptrStreamConfig->GetStreamCaps(nIndex, &lpMediaType, reinterpret_cast(&m_ascStreamCfgCaps)); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + break; + + hResult = ptrStreamConfig->SetFormat(lpMediaType); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + break; + + m_nCapSelected = nIndex; + + } while (FALSE); + + CDShowHelper::FreeMediaType(lpMediaType); + + return SUCCEEDED(hResult); +} + +BOOL CAGDShowAudioCapture::FindPin(IPin **ppInputPin, IPin **ppOutputPin) +{ + HRESULT hResult = S_OK; + + CComPtr ptrCaptureFilter = NULL; + CComPtr ptrSourceFilter = NULL; + + hResult = m_ptrGraphBuilder->FindFilterByName(filterCaptureName, &ptrCaptureFilter); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return FALSE; + + hResult = m_ptrGraphBuilder->FindFilterByName(filterSourceName, &ptrSourceFilter); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return FALSE; + + return TRUE; +} + +BOOL CAGDShowAudioCapture::GetCaptureBuffer(SIZE_T *nBlockSize, SIZE_T *nBlockCount, SIZE_T *nAlign) +{ + HRESULT hResult = S_OK; + CComPtr ptrCaptureFilter = NULL; + CComPtr ptrBufferNegotiation = NULL; + + hResult = m_ptrGraphBuilder->FindFilterByName(L"Source", &ptrCaptureFilter); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return FALSE; + + hResult = m_ptrCaptureGraphBuilder2->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Audio, ptrCaptureFilter, IID_IAMBufferNegotiation, (void**)&ptrBufferNegotiation); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return FALSE; + + ALLOCATOR_PROPERTIES alcProper; + + memset(&alcProper, 0, sizeof(ALLOCATOR_PROPERTIES)); + hResult = ptrBufferNegotiation->GetAllocatorProperties(&alcProper); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return FALSE; + + *nBlockSize = alcProper.cbBuffer; + *nBlockCount = alcProper.cBuffers; + *nAlign = alcProper.cbAlign; + + return TRUE; +} + +BOOL CAGDShowAudioCapture::SetCaptureBuffer(SIZE_T nBlockSize, SIZE_T nBlockCount, SIZE_T nAlign) +{ + HRESULT hResult = S_OK; + CComPtr ptrCaptureFilter = NULL; + CComPtr ptrBufferNegotiation = NULL; + + hResult = m_ptrGraphBuilder->FindFilterByName(filterSourceName, &ptrCaptureFilter); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return FALSE; + + hResult = m_ptrCaptureGraphBuilder2->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Audio, ptrCaptureFilter, IID_IAMBufferNegotiation, (void**)&ptrBufferNegotiation); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return FALSE; + + ALLOCATOR_PROPERTIES alcProper; + + alcProper.cbBuffer = nBlockSize; + alcProper.cBuffers = nBlockCount; + alcProper.cbAlign = nAlign; + alcProper.cbPrefix = 0; + + hResult = ptrBufferNegotiation->SuggestAllocatorProperties(&alcProper); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return FALSE; + + return TRUE; +} + +BOOL CAGDShowAudioCapture::CaptureControl(int nControlCode) +{ + CComPtr ptrInputPin = NULL; + CComPtr ptrOutputPin = NULL; + + CComQIPtr ptrControl(m_ptrGraphBuilder); + + FindPin(&ptrInputPin, &ptrOutputPin); + + HRESULT hResult = S_OK; + + switch (nControlCode) + { + case DEVICE_START: + m_ptrGraphBuilder->Connect(ptrInputPin, ptrOutputPin); + hResult = ptrControl->Run(); + break; + case DEVICE_STOP: + hResult = ptrControl->Stop(); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return FALSE; + + hResult = m_ptrGraphBuilder->Disconnect(ptrInputPin); + hResult = m_ptrGraphBuilder->Disconnect(ptrOutputPin); + break; + default: + ATLASSERT(FALSE); + break; + } + + ATLASSERT(SUCCEEDED(hResult)); + + return hResult == S_OK ? TRUE : FALSE; +} + +BOOL CAGDShowAudioCapture::GetCurrentMediaType(AM_MEDIA_TYPE **pMediaType) +{ + BOOL bSuccess = FALSE; + HRESULT hResult = S_OK; + + CComPtr ptrCaptureFilter = nullptr; + CComPtr ptrStreamConfig = nullptr; + + hResult = m_ptrGraphBuilder->FindFilterByName(filterSourceName, &ptrCaptureFilter); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return FALSE; + + hResult = m_ptrCaptureGraphBuilder2->FindInterface(&PIN_CATEGORY_CAPTURE, &MEDIATYPE_Audio, ptrCaptureFilter, IID_IAMStreamConfig, (void**)&ptrStreamConfig); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return FALSE; + + hResult = ptrStreamConfig->GetFormat(pMediaType); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) + return FALSE; + + return TRUE; +} +BOOL CAGDShowAudioCapture::CreateCaptureFilter() +{ + if (audioCapture) { + m_ptrGraphBuilder->RemoveFilter(audioCapture); + audioCapture.Release(); + } + + AM_MEDIA_TYPE* mt; + if (GetCurrentMediaType(&mt)) { + PinCaptureInfo info; + info.callback = [this](IMediaSample *s) {Receive(false, s); }; + info.expectedMajorType = mt->majortype; + info.expectedSubType = mt->subtype; + audioCapture = new CaptureFilter(info); + + HRESULT hr = m_ptrGraphBuilder->AddFilter(audioCapture, filterCaptureName); + IBaseFilter* filter; + + m_ptrGraphBuilder->FindFilterByName(filterCaptureName,&filter); + if (SUCCEEDED(hr)) + return TRUE; + + CDShowHelper::FreeMediaType(mt); + } + + return FALSE; +} +BOOL CAGDShowAudioCapture::Start() +{ + if (ConnectFilters()) { + control->Run(); + active = true; + return TRUE; + } + return FALSE; +} +void CAGDShowAudioCapture::Stop() +{ + if (active) { + control->Stop(); + active = false; + } + + DisconnectPins(); +} + +void CAGDShowAudioCapture::Receive(bool video, IMediaSample *sample) +{ + BYTE *pBuffer; + if (!sample) + return; + + int size = sample->GetActualDataLength(); + if (!size) + return; + + if (FAILED(sample->GetPointer(&pBuffer))) + return; + + CircleBuffer::GetInstance()->writeBuffer(pBuffer, size, GetTickCount()); +} + +void CAGDShowAudioCapture::GetDeviceName(LPTSTR deviceName, SIZE_T *nDeviceLen) +{ + for (size_t i = 0; i < m_listDeviceInfo.GetCount(); ++i) { + POSITION pos = m_listDeviceInfo.FindIndex(i); + AGORA_DEVICE_INFO &agDeviceInfo = m_listDeviceInfo.GetAt(pos); + if (_tcscmp(m_szActiveDeviceID, agDeviceInfo.szDevicePath) == 0) { + *nDeviceLen = _tcslen(agDeviceInfo.szDeviceName); + _tcscpy_s(deviceName, *nDeviceLen + 1, agDeviceInfo.szDeviceName); + + break; + } + } +} + +BOOL CAGDShowAudioCapture::ConnectFilters() +{ + CComPtr filter = nullptr; + HRESULT hResult = m_ptrGraphBuilder->FindFilterByName(filterSourceName, &filter); + TCHAR deviceName[MAX_PATH] = { 0 }; + SIZE_T len = 0; + GetDeviceName(deviceName, &len); + if (SUCCEEDED(hResult) && filter && audioCapture) { + bool success = ConnectPins(PIN_CATEGORY_CAPTURE, + MEDIATYPE_Audio, filter, + audioCapture); + return TRUE; + } + + return FALSE; +} + +BOOL CAGDShowAudioCapture::ConnectPins(const GUID &category, const GUID &type, + IBaseFilter *filter, IBaseFilter *capture) +{ + HRESULT hr = S_OK; + + if (!CDShowHelper::GetFilterPin(filter, type, category, PINDIR_OUTPUT, &filterPin)) { + OutputDebugString(L"Failed to find pin"); + return FALSE; + } + + if (!CDShowHelper::GetPinByName(capture, PINDIR_INPUT, nullptr, &capturePin)) { + OutputDebugString(L"Failed to find capture pin"); + return FALSE; + } + OutputDebugString(L"ConnectDirect\n"); + hr = m_ptrGraphBuilder->ConnectDirect(filterPin, capturePin, nullptr); + if (FAILED(hr)) { + OutputDebugString(L"failed to connect pins"); + return FALSE; + } + + return TRUE; +} + +BOOL CAGDShowAudioCapture::DisconnectPins() +{ + if (!m_ptrGraphBuilder) + return FALSE; + + if (!capturePin && !filterPin) + return FALSE; + HRESULT hr = S_FALSE; + if (capturePin) { + hr = m_ptrGraphBuilder->Disconnect(capturePin); + capturePin.Release(); + } + ATLASSERT(SUCCEEDED(hr)); + + if (filterPin) { + hr = m_ptrGraphBuilder->Disconnect(filterPin); + filterPin.Release(); + } + ATLASSERT(SUCCEEDED(hr)); + return TRUE; +} \ No newline at end of file diff --git a/windows/APIExample/APIExample/DirectShow/AGDShowAudioCapture.h b/windows/APIExample/APIExample/DirectShow/AGDShowAudioCapture.h new file mode 100644 index 000000000..27d66202e --- /dev/null +++ b/windows/APIExample/APIExample/DirectShow/AGDShowAudioCapture.h @@ -0,0 +1,74 @@ +#pragma once +#include +#include +#include +#include +#include "IAGDShowDevice.h" +#include "capture-filter.hpp" +class CAGDShowAudioCapture + : public IDShowCaptureDevice +{ +public: + + CAGDShowAudioCapture(); + ~CAGDShowAudioCapture(); + + virtual BOOL Create(); + virtual void Close(); + + virtual BOOL EnumDeviceList(); + virtual int GetDeviceCount() { return m_listDeviceInfo.GetCount(); }; + virtual BOOL GetDeviceInfo(int nIndex, LPAGORA_DEVICE_INFO lpDeviceInfo); + + virtual BOOL OpenDevice(int nIndex); + virtual BOOL OpenDevice(LPCTSTR lpDevicePath); + virtual BOOL GetCurrentDevice(LPTSTR lpDevicePath, SIZE_T *nDevicePathLen); + virtual void CloseDevice(); + + virtual int GetMediaCapCount(); + virtual BOOL GetMediaCap(int nIndex, AM_MEDIA_TYPE **ppMediaType, LPVOID lpMediaStreamConfigCaps, SIZE_T nSize); + virtual BOOL SelectMediaCap(int nIndex); + virtual int GetSelectedMediaCap() { return m_nCapSelected; }; + virtual BOOL GetVideoCap(int nIndex, VIDEOINFOHEADER *lpVideoInfo) { return FALSE; } + BOOL GetCaptureBuffer(SIZE_T *nBlockSize, SIZE_T *nBlockCount, SIZE_T *nAlign); + BOOL SetCaptureBuffer(SIZE_T nBlockSize, SIZE_T nBlockCount, SIZE_T nAlign); + + virtual BOOL GetCurrentVideoCap(VIDEOINFOHEADER *lpVideoInfo) { return FALSE; } + virtual BOOL GetCurrentMediaType(AM_MEDIA_TYPE **pMediaType); + virtual BOOL CreateCaptureFilter(); + virtual BOOL Start(); + virtual void Stop(); + + virtual BOOL CaptureControl(int nControlCode = DEVICE_START); + + virtual BOOL GetAudioCap(int nIndex, WAVEFORMATEX *lpWaveInfo); + virtual BOOL GetCurrentAudioCap(WAVEFORMATEX *lpWaveInfo); + +protected: + BOOL ConnectFilters(); + BOOL ConnectPins(const GUID &category, const GUID &type, + IBaseFilter *filter, IBaseFilter *capture); + BOOL DisconnectPins(); + void Receive(bool video, IMediaSample *sample); + void GetDeviceName(LPTSTR deviceName, SIZE_T *nDeviceLen); + BOOL FindPin(IPin **ppInputPin, IPin **ppOutputPin); +private: + TCHAR m_szActiveDeviceID[MAX_PATH]; + CAtlList m_listDeviceInfo; + + AUDIO_STREAM_CONFIG_CAPS m_ascStreamCfgCaps; + int m_nCapSelected; + + // private dshow object +private: + CComPtr m_ptrGraphBuilder; + CComPtr m_ptrCaptureGraphBuilder2; + CComPtr control; + CComPtr audioCapture; + AM_MEDIA_TYPE* curMT = nullptr; + CComPtr filterPin = nullptr; + CComPtr capturePin = nullptr; + CString filterSourceName; + CString filterCaptureName; + bool active = false; +}; diff --git a/windows/APIExample/APIExample/DirectShow/AGDShowVideoCapture.cpp b/windows/APIExample/APIExample/DirectShow/AGDShowVideoCapture.cpp new file mode 100644 index 000000000..c98ddcdc3 --- /dev/null +++ b/windows/APIExample/APIExample/DirectShow/AGDShowVideoCapture.cpp @@ -0,0 +1,636 @@ +#define HAVE_JPEG + +#include "AGDShowVideoCapture.h" +#include +#include "AgVideoBuffer.h" +#include "DShowHelper.h" +#include "libyuv.h" +#ifdef DEBUG +#pragma comment(lib, "yuv.lib") +#pragma comment(lib, "jpeg-static.lib") +#else +#pragma comment(lib, "yuv.lib") +#pragma comment(lib, "jpeg-static.lib") +#endif + +using namespace libyuv; +#define MAX_VIDEO_BUFFER_SIZE (4 * 1920 * 1080 * 4) // 4K RGBA max size +CAGDShowVideoCapture::CAGDShowVideoCapture() + : m_ptrGraphBuilder(nullptr), + m_ptrCaptureGraphBuilder2(nullptr), + m_nCapSelected(-1) { + memset(m_szActiveDeviceID, 0, MAX_PATH * sizeof(TCHAR)); + m_lpYUVBuffer = new BYTE[MAX_VIDEO_BUFFER_SIZE]; + filterName = L"Video Filter"; +} + +CAGDShowVideoCapture::~CAGDShowVideoCapture() { + Close(); + if (m_lpYUVBuffer) { + delete[] m_lpYUVBuffer; + m_lpYUVBuffer = nullptr; + } +} + +BOOL CAGDShowVideoCapture::Create() { + HRESULT hResult = S_OK; + BOOL bRet = FALSE; + do { + if (S_OK != CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER, + IID_IFilterGraph, (void **)&m_ptrGraphBuilder)) + break; + + if (S_OK != CoCreateInstance(CLSID_CaptureGraphBuilder2, NULL, + CLSCTX_INPROC_SERVER, + IID_ICaptureGraphBuilder2, + (void **)&m_ptrCaptureGraphBuilder2)) + break; + + if (S_OK != m_ptrCaptureGraphBuilder2->SetFiltergraph(m_ptrGraphBuilder)) + break; + + if (S_OK != + m_ptrGraphBuilder->QueryInterface(IID_IMediaControl, (void **)&control)) + break; + + bRet = TRUE; + } while (false); + return bRet; +} + +void CAGDShowVideoCapture::Close() { + CComPtr filterEnum = nullptr; + HRESULT hr; + + if (!m_ptrGraphBuilder) return; + + hr = m_ptrGraphBuilder->EnumFilters(&filterEnum); + if (FAILED(hr)) return; + + CComPtr filter = nullptr; + while (filterEnum->Next(1, &filter, nullptr) == S_OK) { + m_ptrGraphBuilder->RemoveFilter(filter); + filterEnum->Reset(); + filter.Release(); + } + + m_ptrGraphBuilder.Release(); + m_ptrCaptureGraphBuilder2.Release(); +} + +BOOL CAGDShowVideoCapture::EnumDeviceList() { + HRESULT hResult = S_OK; + + CComVariant var; + WCHAR *wszDevicePath = nullptr; + + CComPtr ptrCreateDevEnum = nullptr; + CComPtr ptrEnumMoniker = nullptr; + CComPtr ptrMoniker = nullptr; + + AGORA_DEVICE_INFO agDeviceInfo; + + hResult = CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER, + IID_ICreateDevEnum, (void **)&ptrCreateDevEnum); + if (FAILED(hResult)) return FALSE; + + hResult = ptrCreateDevEnum->CreateClassEnumerator( + CLSID_VideoInputDeviceCategory, &ptrEnumMoniker, 0); + if (FAILED(hResult)) return FALSE; + + m_listDeviceInfo.RemoveAll(); + + do { + hResult = ptrEnumMoniker->Next(1, &ptrMoniker, nullptr); + if (hResult != S_OK) break; + IBaseFilter *filter; + if (SUCCEEDED(ptrMoniker->BindToObject(NULL, 0, IID_IBaseFilter, + (void **)&filter))) { + CComPtr ptrPropertyBag = nullptr; + + hResult = ptrMoniker->BindToStorage(nullptr, nullptr, IID_IPropertyBag, + (void **)(&ptrPropertyBag)); + if (hResult != S_OK) break; + + memset(&agDeviceInfo, 0, sizeof(AGORA_DEVICE_INFO)); + + var.Clear(); + hResult = ptrPropertyBag->Read(L"FriendlyName", &var, nullptr); + + if (SUCCEEDED(hResult)) { +#ifdef UNICODE + _tcscpy_s(agDeviceInfo.szDeviceName, var.bstrVal); +#else + ::WideCharToMultiByte(CP_ACP, 0, var.bstrVal, -1, + agDeviceInfo.szDeviceName, MAX_PATH, nullptr, + nullptr); +#endif + } + var.Clear(); + hResult = ptrPropertyBag->Read(_T("DevicePath"), &var, nullptr); + if (SUCCEEDED(hResult)) { + _tcscpy_s(agDeviceInfo.szDevicePath, var.bstrVal); + } + + m_listDeviceInfo.AddTail(agDeviceInfo); + } + if (ptrMoniker) ptrMoniker.Release(); + + } while (TRUE); + + return TRUE; +} + +BOOL CAGDShowVideoCapture::GetDeviceInfo(int nIndex, + LPAGORA_DEVICE_INFO lpDeviceInfo) { + ATLASSERT(lpDeviceInfo != nullptr); + ATLASSERT(nIndex >= 0 && + nIndex < static_cast(m_listDeviceInfo.GetCount())); + + POSITION pos = m_listDeviceInfo.FindIndex(nIndex); + if (pos == nullptr) return FALSE; + + AGORA_DEVICE_INFO &agDeviceInfo = m_listDeviceInfo.GetAt(pos); + memcpy(lpDeviceInfo, &agDeviceInfo, sizeof(AGORA_DEVICE_INFO)); + + return TRUE; +} + +BOOL CAGDShowVideoCapture::OpenDevice(int nIndex) { + ATLASSERT(nIndex >= 0 && + nIndex < static_cast(m_listDeviceInfo.GetCount())); + + m_nCapSelected = -1; + POSITION pos = m_listDeviceInfo.FindIndex(nIndex); + if (pos == nullptr) return FALSE; + + LPCTSTR lpDevicePath = m_listDeviceInfo.GetAt(pos).szDevicePath; + + return OpenDevice(lpDevicePath, m_listDeviceInfo.GetAt(pos).szDeviceName); +} + +BOOL CAGDShowVideoCapture::OpenDevice(LPCTSTR lpDevicePath, + LPCTSTR lpDeviceName) { + HRESULT hResult = S_OK; + hResult = m_ptrGraphBuilder->RemoveFilter(videoFilter); + videoFilter.Release(); + IBaseFilter *filter = nullptr; + if (CDShowHelper::GetDeviceFilter(CLSID_VideoInputDeviceCategory, + lpDeviceName, lpDevicePath, &filter)) { + hResult = m_ptrGraphBuilder->AddFilter(filter, filterName); + videoFilter = filter; + ATLASSERT(SUCCEEDED(hResult)); + if (hResult != S_OK ) return FALSE; + m_currentDeviceName = lpDeviceName; + _tcscpy_s(m_szActiveDeviceID, MAX_PATH, lpDevicePath); + SelectMediaCap(0); + return TRUE; + } + + return FALSE; +} + +BOOL CAGDShowVideoCapture::GetCurrentDevice(LPTSTR lpDevicePath, + SIZE_T *nDevicePathLen) { + int nDeviceLen = _tcslen(m_szActiveDeviceID); + if (nDeviceLen >= static_cast(*nDevicePathLen)) { + *nDevicePathLen = nDeviceLen + 1; + return FALSE; + } + + if (nDeviceLen == 0) return FALSE; + + _tcscpy_s(lpDevicePath, *nDevicePathLen, m_szActiveDeviceID); + *nDevicePathLen = nDeviceLen + 1; + + return TRUE; +} + +void CAGDShowVideoCapture::CloseDevice() { + HRESULT hResult = S_OK; + CComPtr ptrCaptureFilter = nullptr; + hResult = m_ptrGraphBuilder->FindFilterByName(filterName, &ptrCaptureFilter); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) return; + m_ptrGraphBuilder->RemoveFilter(ptrCaptureFilter); + + ZeroMemory(m_szActiveDeviceID, MAX_PATH * sizeof(TCHAR)); +} + +int CAGDShowVideoCapture::GetMediaCapCount() { + int nCount = 0; + int nSize = 0; + HRESULT hResult = S_OK; + + CComPtr ptrCaptureFilter = nullptr; + CComPtr ptrStreamConfig = nullptr; + + hResult = m_ptrGraphBuilder->FindFilterByName(filterName, &ptrCaptureFilter); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) return 0; + + hResult = m_ptrCaptureGraphBuilder2->FindInterface( + &PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, ptrCaptureFilter, + IID_IAMStreamConfig, (void **)&ptrStreamConfig); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) return 0; + + hResult = ptrStreamConfig->GetNumberOfCapabilities(&nCount, &nSize); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) return 0; + + return nCount; +} + +BOOL CAGDShowVideoCapture::GetMediaCap(int nIndex, AM_MEDIA_TYPE **ppMediaType, + LPVOID lpMediaStreamConfigCaps, + SIZE_T nSize) { + int nCount = 0; + int nCapSize = 0; + HRESULT hResult = S_OK; + + CComPtr ptrCaptureFilter = nullptr; + CComPtr ptrStreamConfig = nullptr; + + hResult = m_ptrGraphBuilder->FindFilterByName(filterName, &ptrCaptureFilter); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) return FALSE; + + hResult = m_ptrCaptureGraphBuilder2->FindInterface( + &PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, ptrCaptureFilter, + IID_IAMStreamConfig, (void **)&ptrStreamConfig); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) return FALSE; + + hResult = ptrStreamConfig->GetNumberOfCapabilities(&nCount, &nCapSize); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) return FALSE; + + ATLASSERT(nCapSize <= static_cast(nSize)); + if (nCapSize > static_cast(nSize)) return FALSE; + + hResult = ptrStreamConfig->GetStreamCaps( + nIndex, ppMediaType, reinterpret_cast(lpMediaStreamConfigCaps)); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) return FALSE; + + return TRUE; +} + +BOOL CAGDShowVideoCapture::SelectMediaCap(int nIndex) { + int nCount = 0; + int nSize = 0; + HRESULT hResult = S_OK; + + AM_MEDIA_TYPE *lpMediaType = NULL; + + CComPtr ptrCaptureFilter = nullptr; + CComPtr ptrStreamConfig = nullptr; + + hResult = m_ptrGraphBuilder->FindFilterByName(filterName, &ptrCaptureFilter); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) return FALSE; + + hResult = m_ptrCaptureGraphBuilder2->FindInterface( + &PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, ptrCaptureFilter, + IID_IAMStreamConfig, (void **)&ptrStreamConfig); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) return FALSE; + + hResult = ptrStreamConfig->GetNumberOfCapabilities(&nCount, &nSize); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) return FALSE; + + ATLASSERT(nIndex >= 0 && nIndex < nCount); + if (nIndex < 0 || nIndex >= nCount) nIndex = 0; + + ATLASSERT(nSize <= sizeof(VIDEO_STREAM_CONFIG_CAPS)); + + do { + hResult = ptrStreamConfig->GetStreamCaps( + nIndex, &lpMediaType, reinterpret_cast(&m_vscStreamCfgCaps)); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) break; + + hResult = ptrStreamConfig->SetFormat(lpMediaType); + /* ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) break;*/ + + } while (FALSE); + + CDShowHelper::FreeMediaType(lpMediaType); + + return SUCCEEDED(hResult); +} + +BOOL CAGDShowVideoCapture::GetVideoCap(int nIndex, + VIDEOINFOHEADER *lpVideoInfo) { + int nCount = 0; + int nSize = 0; + + AM_MEDIA_TYPE *lpAMMediaType = NULL; + VIDEO_STREAM_CONFIG_CAPS videoStreamCfgCaps; + + BOOL bSuccess = GetMediaCap(nIndex, &lpAMMediaType, &videoStreamCfgCaps, + sizeof(VIDEO_STREAM_CONFIG_CAPS)); + + if (lpAMMediaType->formattype == FORMAT_VideoInfo) { + VIDEOINFOHEADER *pVideoInfo = + reinterpret_cast(lpAMMediaType->pbFormat); + memcpy_s(lpVideoInfo, sizeof(VIDEOINFOHEADER), pVideoInfo, + sizeof(VIDEOINFOHEADER)); + + bSuccess = TRUE; + } else if (lpAMMediaType->formattype == FORMAT_VideoInfo2) { + VIDEOINFOHEADER2 *pVideoInfo2 = + reinterpret_cast(lpAMMediaType->pbFormat); + memcpy_s(&lpVideoInfo->bmiHeader, sizeof(BITMAPINFOHEADER), + &pVideoInfo2->bmiHeader, sizeof(BITMAPINFOHEADER)); + lpVideoInfo->AvgTimePerFrame = pVideoInfo2->AvgTimePerFrame; + lpVideoInfo->dwBitErrorRate = pVideoInfo2->dwBitErrorRate; + lpVideoInfo->dwBitRate = pVideoInfo2->dwBitRate; + bSuccess = TRUE; + } else + bSuccess = FALSE; + + CDShowHelper::FreeMediaType(lpAMMediaType); + + return bSuccess; +} + +BOOL CAGDShowVideoCapture::GetCurrentVideoCap(VIDEOINFOHEADER *lpVideoInfo) { + BOOL bSuccess = FALSE; + HRESULT hResult = S_OK; + AM_MEDIA_TYPE *lpAMMediaType = NULL; + + CComPtr ptrCaptureFilter = nullptr; + CComPtr ptrStreamConfig = nullptr; + + hResult = m_ptrGraphBuilder->FindFilterByName(filterName, &ptrCaptureFilter); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) return FALSE; + + hResult = m_ptrCaptureGraphBuilder2->FindInterface( + &PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, ptrCaptureFilter, + IID_IAMStreamConfig, (void **)&ptrStreamConfig); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) return FALSE; + + hResult = ptrStreamConfig->GetFormat(&lpAMMediaType); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) return FALSE; + + if (lpAMMediaType->formattype == FORMAT_VideoInfo) { + VIDEOINFOHEADER *pVideoInfo = + reinterpret_cast(lpAMMediaType->pbFormat); + memcpy_s(lpVideoInfo, sizeof(VIDEOINFOHEADER), pVideoInfo, + sizeof(VIDEOINFOHEADER)); + + bSuccess = TRUE; + } else if (lpAMMediaType->formattype == FORMAT_VideoInfo2) { + VIDEOINFOHEADER2 *pVideoInfo2 = + reinterpret_cast(lpAMMediaType->pbFormat); + memcpy_s(&lpVideoInfo->bmiHeader, sizeof(BITMAPINFOHEADER), + &pVideoInfo2->bmiHeader, sizeof(BITMAPINFOHEADER)); + lpVideoInfo->AvgTimePerFrame = pVideoInfo2->AvgTimePerFrame; + lpVideoInfo->dwBitErrorRate = pVideoInfo2->dwBitErrorRate; + lpVideoInfo->dwBitRate = pVideoInfo2->dwBitRate; + bSuccess = TRUE; + } else + bSuccess = FALSE; + + CDShowHelper::FreeMediaType(lpAMMediaType); + + return bSuccess; +} + +BOOL CAGDShowVideoCapture::RemoveCaptureFilter() { + if (videoCapture) { + m_ptrGraphBuilder->RemoveFilter(videoCapture); + videoCapture.Release(); + return TRUE; + } + return FALSE; +} + +BOOL CAGDShowVideoCapture::CreateCaptureFilter() { + m_ptrGraphBuilder->RemoveFilter(videoCapture); + if (videoCapture) { + videoCapture.Release(); + } + + AM_MEDIA_TYPE *mt = nullptr; + if (GetCurrentMediaType(&mt)) { + PinCaptureInfo info; + info.callback = [this](IMediaSample *s) { Receive(true, s); }; + info.expectedMajorType = mt->majortype; + info.expectedSubType = mt->subtype; + videoCapture = new CaptureFilter(info); + + bmiHeader = CDShowHelper::GetBitmapInfoHeader(*mt); + // CVideoPackageQueue::GetInstance()->SetVideoFormat(bmiHeader); + HRESULT hr = + m_ptrGraphBuilder->AddFilter(videoCapture, L"Video Capture Filter"); + if (SUCCEEDED(hr)) return TRUE; + CDShowHelper::FreeMediaType(mt); + } + return FALSE; +} + +BOOL CAGDShowVideoCapture::Start() { + if (ConnectFilters()) { + control->Run(); + active = true; + return TRUE; + } + return FALSE; +} + +void CAGDShowVideoCapture::Stop() { + if (active) { + control->Stop(); + active = false; + } +} + +void CAGDShowVideoCapture::GetDeviceName(LPTSTR deviceName, + SIZE_T *nDeviceLen) { + for (size_t i = 0; i < m_listDeviceInfo.GetCount(); ++i) { + POSITION pos = m_listDeviceInfo.FindIndex(i); + AGORA_DEVICE_INFO &agDeviceInfo = m_listDeviceInfo.GetAt(pos); + if (_tcscmp(m_szActiveDeviceID, agDeviceInfo.szDevicePath) == 0) { + *nDeviceLen = _tcslen(agDeviceInfo.szDeviceName); + _tcscpy_s(deviceName, *nDeviceLen + 1, agDeviceInfo.szDeviceName); + + break; + } + } +} + +BOOL CAGDShowVideoCapture::ConnectFilters() { + CComPtr filter = nullptr; + HRESULT hResult = m_ptrGraphBuilder->FindFilterByName(filterName, &filter); + TCHAR deviceName[MAX_PATH] = {0}; + SIZE_T len = 0; + GetDeviceName(deviceName, &len); + if (SUCCEEDED(hResult) && filter && videoCapture) { + bool success = ConnectPins(PIN_CATEGORY_CAPTURE, MEDIATYPE_Video, filter, + videoCapture); + return TRUE; + } + + return FALSE; +} + +BOOL CAGDShowVideoCapture::ConnectPins(const GUID &category, const GUID &type, + IBaseFilter *filter, + IBaseFilter *capture) { + HRESULT hr = S_OK; + CComPtr filterPin = nullptr; + CComPtr capturePin = nullptr; + + if (!CDShowHelper::GetFilterPin(filter, type, category, PINDIR_OUTPUT, + &filterPin)) { + OutputDebugString(L"Failed to find pin"); + return FALSE; + } + + if (!CDShowHelper::GetPinByName(capture, PINDIR_INPUT, nullptr, + &capturePin)) { + OutputDebugString(L"Failed to find capture pin"); + return FALSE; + } + OutputDebugString(L"ConnectDirect\n"); + hr = m_ptrGraphBuilder->ConnectDirect(filterPin, capturePin, nullptr); + if (FAILED(hr)) { + OutputDebugString(L"failed to connect pins"); + return FALSE; + } + + return TRUE; +} + +BOOL CAGDShowVideoCapture::GetCurrentMediaType(AM_MEDIA_TYPE **lpAMMediaType) { + BOOL bSuccess = FALSE; + HRESULT hResult = S_OK; + + CComPtr ptrCaptureFilter = nullptr; + CComPtr ptrStreamConfig = nullptr; + + hResult = m_ptrGraphBuilder->FindFilterByName(filterName, &ptrCaptureFilter); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) return FALSE; + + hResult = m_ptrCaptureGraphBuilder2->FindInterface( + &PIN_CATEGORY_CAPTURE, &MEDIATYPE_Video, ptrCaptureFilter, + IID_IAMStreamConfig, (void **)&ptrStreamConfig); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) return FALSE; + + hResult = ptrStreamConfig->GetFormat(lpAMMediaType); + ATLASSERT(SUCCEEDED(hResult)); + if (FAILED(hResult)) return FALSE; + + return TRUE; +} + +void CAGDShowVideoCapture::Receive(bool video, IMediaSample *sample) { + BYTE *pBuffer; + if (!sample) return; + + int size = sample->GetActualDataLength(); + if (!size) return; + + if (FAILED(sample->GetPointer(&pBuffer))) return; + long long startTime, stopTime; + bool hasTime = SUCCEEDED(sample->GetTime(&startTime, &stopTime)); + +#ifdef DEBUG + HANDLE hFile = INVALID_HANDLE_VALUE; + DWORD dwBytesWritten = 0; + + switch (bmiHeader->biCompression) { + case 0x00000000: // RGB24 + hFile = ::CreateFile(_T("d:\\pictest\\test.rgb24"), GENERIC_WRITE, + FILE_SHARE_READ, NULL, CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, NULL); + break; + case MAKEFOURCC('I', '4', '2', '0'): // I420 + hFile = ::CreateFile(_T("d:\\pictest\\test.i420"), GENERIC_WRITE, + FILE_SHARE_READ, NULL, CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, NULL); + break; + case MAKEFOURCC('Y', 'U', 'Y', '2'): // YUY2 + hFile = ::CreateFile(_T("d:\\pictest\\test.yuy2"), GENERIC_WRITE, + FILE_SHARE_READ, NULL, CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, NULL); + break; + case MAKEFOURCC('M', 'J', 'P', 'G'): // MJPEG + hFile = ::CreateFile(_T("d:\\pictest\\test.jpeg"), GENERIC_WRITE, + FILE_SHARE_READ, NULL, CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, NULL); + break; + case MAKEFOURCC('U', 'Y', 'V', 'Y'): // UYVY + hFile = ::CreateFile(_T("d:\\pictest\\test.uyvy"), GENERIC_WRITE, + FILE_SHARE_READ, NULL, CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, NULL); + break; + default: + break; + } + + if (hFile != INVALID_HANDLE_VALUE) { + ::WriteFile(hFile, pBuffer, size, &dwBytesWritten, NULL); + ::CloseHandle(hFile); + } +#endif + m_lpY = m_lpYUVBuffer; + m_lpU = m_lpY + bmiHeader->biWidth * bmiHeader->biHeight; + m_lpV = m_lpU + bmiHeader->biWidth * bmiHeader->biHeight / 4; + switch (bmiHeader->biCompression) { + case 0x00000000: // RGB24 + RGB24ToI420(pBuffer, bmiHeader->biWidth * 3, m_lpY, bmiHeader->biWidth, + m_lpU, bmiHeader->biWidth / 2, m_lpV, bmiHeader->biWidth / 2, + bmiHeader->biWidth, bmiHeader->biHeight); + break; + case MAKEFOURCC('I', '4', '2', '0'): // I420 + memcpy_s(m_lpYUVBuffer, 0x800000, pBuffer, size); + break; + case MAKEFOURCC('Y', 'U', 'Y', '2'): // YUY2 + YUY2ToI420(pBuffer, bmiHeader->biWidth * 2, m_lpY, bmiHeader->biWidth, + m_lpU, bmiHeader->biWidth / 2, m_lpV, bmiHeader->biWidth / 2, + bmiHeader->biWidth, bmiHeader->biHeight); + break; + case MAKEFOURCC('M', 'J', 'P', 'G'): // MJPEG + MJPGToI420(pBuffer, size, m_lpY, bmiHeader->biWidth, m_lpU, + bmiHeader->biWidth / 2, m_lpV, bmiHeader->biWidth / 2, + bmiHeader->biWidth, bmiHeader->biHeight, bmiHeader->biWidth, + bmiHeader->biHeight); + break; + case MAKEFOURCC('U', 'Y', 'V', 'Y'): // UYVY + UYVYToI420(pBuffer, bmiHeader->biWidth, m_lpY, bmiHeader->biWidth, m_lpU, + bmiHeader->biWidth / 2, m_lpV, bmiHeader->biWidth / 2, + bmiHeader->biWidth, bmiHeader->biHeight); + break; + default: + ATLASSERT(FALSE); + break; + } + SIZE_T nYUVSize = bmiHeader->biWidth * bmiHeader->biHeight * 3 / 2; + if (!CAgVideoBuffer::GetInstance()->writeBuffer(m_lpYUVBuffer, nYUVSize, + GetTickCount())) { + OutputDebugString(L"CAgVideoBuffer::GetInstance()->writeBuffer failed."); + return; + } +#ifdef DEBUG + hFile = ::CreateFile(_T("d:\\pictest\\trans.i420"), GENERIC_WRITE, + FILE_SHARE_READ, NULL, CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, NULL); + + if (hFile != INVALID_HANDLE_VALUE) { + ::WriteFile(hFile, m_lpYUVBuffer, nYUVSize, &dwBytesWritten, NULL); + ::CloseHandle(hFile); + } + +#endif +} diff --git a/windows/APIExample/APIExample/DirectShow/AGDShowVideoCapture.h b/windows/APIExample/APIExample/DirectShow/AGDShowVideoCapture.h new file mode 100644 index 000000000..fa5e0782d --- /dev/null +++ b/windows/APIExample/APIExample/DirectShow/AGDShowVideoCapture.h @@ -0,0 +1,78 @@ +#pragma once +#include +#include +#include +#include "IAGDShowDevice.h" +#include "capture-filter.hpp" +#include +class CAGDShowVideoCapture + : public IDShowCaptureDevice +{ +public: + CAGDShowVideoCapture(); + ~CAGDShowVideoCapture(); + + virtual BOOL Create(); + virtual void Close(); + + virtual BOOL EnumDeviceList(); + virtual int GetDeviceCount() { return m_listDeviceInfo.GetCount(); }; + virtual BOOL GetDeviceInfo(int nIndex, LPAGORA_DEVICE_INFO lpDeviceInfo); + + virtual BOOL OpenDevice(int nIndex); + + virtual BOOL GetCurrentDevice(LPTSTR lpDevicePath, SIZE_T *nDevicePathLen); + virtual void CloseDevice(); + + virtual int GetMediaCapCount(); + virtual BOOL GetMediaCap(int nIndex, AM_MEDIA_TYPE **ppMediaType, LPVOID lpMediaStreamConfigCaps, SIZE_T nSize); + virtual BOOL SelectMediaCap(int nIndex); + virtual int GetSelectedMediaCap() { return m_nCapSelected; }; + + virtual BOOL GetVideoCap(int nIndex, VIDEOINFOHEADER *lpVideoInfo); + virtual BOOL GetCurrentVideoCap(VIDEOINFOHEADER *lpVideoInfo); + + BOOL RemoveCaptureFilter(); + + virtual BOOL GetCurrentMediaType(AM_MEDIA_TYPE **pMediaType); + virtual BOOL CreateCaptureFilter(); + virtual BOOL Start(); + virtual void Stop(); + + virtual BOOL GetAudioCap(int nIndex, WAVEFORMATEX *lpWaveInfo) { return FALSE; } + virtual BOOL GetCurrentAudioCap(WAVEFORMATEX *lpWaveInfo) { return FALSE; } +private: + BOOL ConnectFilters(); + BOOL ConnectPins(const GUID &category, const GUID &type, + IBaseFilter *filter, IBaseFilter *capture); + BOOL OpenDevice(LPCTSTR lpDevicePath, LPCTSTR lpDeviceName); + void GetDeviceName(LPTSTR deviceName, SIZE_T *nDeviceLen); + void Receive(bool video, IMediaSample *sample); +protected: + BOOL FindPin(IPin **ppInputPin, IPin **ppOutputPin); + +private: + TCHAR m_szActiveDeviceID[MAX_PATH]; + CAtlList m_listDeviceInfo; + VIDEO_STREAM_CONFIG_CAPS m_vscStreamCfgCaps; + int m_nCapSelected; + +private: + + CComPtr m_ptrGraphBuilder;//filter graph + CComPtr m_ptrCaptureGraphBuilder2;//filter graph manager + CComPtr control; + CComPtr videoFilter; + CComPtr videoCapture; + AM_MEDIA_TYPE* curMT = nullptr; + BITMAPINFOHEADER* bmiHeader = nullptr; + bool active = false; + CString filterName; + CString m_currentDeviceName = L""; + + LPBYTE m_lpYUVBuffer = nullptr; + LPBYTE m_lpY = nullptr; + LPBYTE m_lpU = nullptr; + LPBYTE m_lpV = nullptr; +}; + diff --git a/windows/APIExample/APIExample/DirectShow/AgVideoBuffer.cpp b/windows/APIExample/APIExample/DirectShow/AgVideoBuffer.cpp new file mode 100644 index 000000000..1b2985e42 --- /dev/null +++ b/windows/APIExample/APIExample/DirectShow/AgVideoBuffer.cpp @@ -0,0 +1,53 @@ +#include "AgVideoBuffer.h" +#include + +std::mutex buf_mutex; +BYTE CAgVideoBuffer::videoBuffer[VIDEO_BUF_SIZE] = { 0 }; + +CAgVideoBuffer* CAgVideoBuffer::GetInstance() +{ + static CAgVideoBuffer agVideoBuffer; + return &agVideoBuffer; +} + +CAgVideoBuffer::CAgVideoBuffer() +{ + +} + +CAgVideoBuffer::~CAgVideoBuffer() +{ + +} + + +void CAgVideoBuffer::SetVideoFormat(const BITMAPINFOHEADER *lpInfoHeader) +{ + memcpy_s(&m_bmiHeader, sizeof(BITMAPINFOHEADER), lpInfoHeader, sizeof(BITMAPINFOHEADER)); + m_nPackageSize = m_bmiHeader.biWidth*m_bmiHeader.biWidth * 3 / 2; + _ASSERT(m_nPackageSize <= VIDEO_BUF_SIZE); +} + +void CAgVideoBuffer::GetVideoFormat(BITMAPINFOHEADER *lpInfoHeader) +{ + memcpy_s(lpInfoHeader, sizeof(BITMAPINFOHEADER), &m_bmiHeader, sizeof(BITMAPINFOHEADER)); +} + +bool CAgVideoBuffer::writeBuffer(BYTE* buffer, int bufsize, int ts) +{ + if ((size_t)bufsize < m_nPackageSize) + return false; + std::lock_guard buf_lock(buf_mutex); + memcpy_s(videoBuffer, bufsize, buffer, bufsize); + timestamp = ts; + return true; +} +bool CAgVideoBuffer::readBuffer(BYTE* buffer, int bufsize, int& ts) +{ + if ((size_t)bufsize < m_nPackageSize) + return false; + std::lock_guard buf_lock(buf_mutex); + memcpy_s(buffer, bufsize, videoBuffer, bufsize); + ts = timestamp; + return true; +} \ No newline at end of file diff --git a/windows/APIExample/APIExample/DirectShow/AgVideoBuffer.h b/windows/APIExample/APIExample/DirectShow/AgVideoBuffer.h new file mode 100644 index 000000000..f11209986 --- /dev/null +++ b/windows/APIExample/APIExample/DirectShow/AgVideoBuffer.h @@ -0,0 +1,23 @@ +#pragma once +#include +#define VIDEO_BUF_SIZE 4*4*1920*1080// +class CAgVideoBuffer +{ +public: + CAgVideoBuffer(); + ~CAgVideoBuffer(); + + void SetVideoFormat(const BITMAPINFOHEADER *lpInfoHeader); + void GetVideoFormat(BITMAPINFOHEADER *lpInfoHeader); + + bool writeBuffer(BYTE* buffer, int bufsize, int ts); + bool readBuffer(BYTE* buffer, int bufsize, int& ts); + + static CAgVideoBuffer* GetInstance(); +private: + static BYTE videoBuffer[VIDEO_BUF_SIZE]; + BITMAPINFOHEADER m_bmiHeader; + SIZE_T m_nPackageSize; + int timestamp; +}; + diff --git a/windows/APIExample/APIExample/DirectShow/CircleBuffer.cpp b/windows/APIExample/APIExample/DirectShow/CircleBuffer.cpp new file mode 100644 index 000000000..4d9a561ab --- /dev/null +++ b/windows/APIExample/APIExample/DirectShow/CircleBuffer.cpp @@ -0,0 +1,188 @@ + +#include "CircleBuffer.hpp" + +CircleBuffer* CircleBuffer::m_pCircleBuffer = nullptr; + +CircleBuffer* CircleBuffer::GetInstance() +{ + if (m_pCircleBuffer) { + return m_pCircleBuffer; + } + m_pCircleBuffer = new CircleBuffer(MAX_AUDIO_SAMPLE_SIZE, 0); + return m_pCircleBuffer; +} + +void CircleBuffer::CloseInstance() +{ + if (m_pCircleBuffer) { + delete m_pCircleBuffer; + m_pCircleBuffer = nullptr; + } +} + +CircleBuffer::CircleBuffer(const unsigned int iBufferSize,int waittimeout) +{ + this->m_iBufferSize = iBufferSize; + this->m_pBuffer = (BYTE*)malloc(iBufferSize); + this->m_iInternalReadCursor = 0; + this->m_iInternalWriteCursor = 0; + this->m_bComplete = FALSE; + this->m_evtDataAvailable = CreateEvent(NULL, FALSE, FALSE, NULL); + InitializeCriticalSection(&this->m_csCircleBuffer); + this->wait_timeout=waittimeout; +} + +CircleBuffer::~CircleBuffer(void) +{ + DeleteCriticalSection(&this->m_csCircleBuffer); + CloseHandle(this->m_evtDataAvailable); + free(this->m_pBuffer); +} + +BOOL CircleBuffer::IsComplete() +{ + return this->m_bComplete; +} + +void CircleBuffer::SetComplete() +{ + EnterCriticalSection(&this->m_csCircleBuffer); + this->m_bComplete = TRUE; + SetEvent(this->m_evtDataAvailable); + LeaveCriticalSection(&this->m_csCircleBuffer); +} + +unsigned int CircleBuffer::getFreeSize() +{ + unsigned int iNumBytesFree; + + EnterCriticalSection(&this->m_csCircleBuffer); + + if (this->m_iInternalWriteCursor < this->m_iInternalReadCursor) + iNumBytesFree = (this->m_iInternalReadCursor - 1) - this->m_iInternalWriteCursor; + else if (this->m_iInternalWriteCursor == this->m_iInternalReadCursor) + iNumBytesFree = this->m_iBufferSize; + else + iNumBytesFree = (this->m_iInternalReadCursor - 1) + (this->m_iBufferSize - this->m_iInternalWriteCursor); + + LeaveCriticalSection(&this->m_csCircleBuffer); + return iNumBytesFree; +} + +unsigned int CircleBuffer::getUsedSize() +{ + return this->m_iBufferSize - this->getFreeSize(); +} + +void CircleBuffer::flushBuffer() +{ + EnterCriticalSection(&this->m_csCircleBuffer); + this->m_iInternalReadCursor = 0; + this->m_iInternalWriteCursor = 0; + LeaveCriticalSection(&this->m_csCircleBuffer); +} + +void CircleBuffer::writeBuffer(const void* pSourceBuffer, const unsigned int iNumBytes, int audioTime) +{ + unsigned int iBytesToWrite = iNumBytes; + BYTE* pSourceReadCursor = (BYTE*)pSourceBuffer; + + EnterCriticalSection(&this->m_csCircleBuffer); + //audioTimeQueue.push(audioTime); +// if (this->m_iInternalWriteCursor >= this->m_iInternalReadCursor) //2_9 avoid memcpy over +// { + unsigned int iChunkSize = this->m_iBufferSize - this->m_iInternalWriteCursor; + if (iChunkSize > iBytesToWrite) + iChunkSize = iBytesToWrite; + + memcpy(this->m_pBuffer + this->m_iInternalWriteCursor, pSourceReadCursor, iChunkSize); + pSourceReadCursor += iChunkSize; + iBytesToWrite -= iChunkSize; + this->m_iInternalWriteCursor += iChunkSize; + + if (this->m_iInternalWriteCursor >= this->m_iBufferSize) + this->m_iInternalWriteCursor -= this->m_iBufferSize; +// } + + if (iBytesToWrite) + { + memcpy(this->m_pBuffer + this->m_iInternalWriteCursor, pSourceReadCursor, iBytesToWrite); + this->m_iInternalWriteCursor += iBytesToWrite; + } + + SetEvent(this->m_evtDataAvailable); + LeaveCriticalSection(&this->m_csCircleBuffer); +} + +BOOL CircleBuffer::readBuffer(void* pDestBuffer, const unsigned int _iBytesToRead, unsigned int* pbBytesRead, int& audioTime) +{ + unsigned int iBytesToRead = _iBytesToRead; + unsigned int iBytesRead = 0; + DWORD dwWaitResult; + BOOL bComplete = FALSE; + + while (iBytesToRead > 0 && bComplete == FALSE) + { + dwWaitResult = WaitForSingleObject(this->m_evtDataAvailable, this->wait_timeout); + if (dwWaitResult == WAIT_TIMEOUT) + { + *pbBytesRead = iBytesRead; + return FALSE; + } + + EnterCriticalSection(&this->m_csCircleBuffer); + + bool bPopTimeStamp = false; + if (this->m_iInternalReadCursor > this->m_iInternalWriteCursor) + { + unsigned int iChunkSize = this->m_iBufferSize - this->m_iInternalReadCursor; + if (iChunkSize > iBytesToRead) + iChunkSize = iBytesToRead; + + memcpy((BYTE*)pDestBuffer + iBytesRead, this->m_pBuffer + this->m_iInternalReadCursor, iChunkSize); + + iBytesRead += iChunkSize; + iBytesToRead -= iChunkSize; + + this->m_iInternalReadCursor += iChunkSize; + if (this->m_iInternalReadCursor >= this->m_iBufferSize) + this->m_iInternalReadCursor -= this->m_iBufferSize; + bPopTimeStamp = true; + } + + if (iBytesToRead && this->m_iInternalReadCursor < this->m_iInternalWriteCursor) + { + unsigned int iChunkSize = this->m_iInternalWriteCursor - this->m_iInternalReadCursor; + if (iChunkSize > iBytesToRead) + iChunkSize = iBytesToRead; + + memcpy((BYTE*)pDestBuffer + iBytesRead, + this->m_pBuffer + this->m_iInternalReadCursor, + iChunkSize); + + iBytesRead += iChunkSize; + iBytesToRead -= iChunkSize; + this->m_iInternalReadCursor += iChunkSize; + bPopTimeStamp = true; + } + + if(bPopTimeStamp && !audioTimeQueue.empty()) { + audioTime = audioTimeQueue.front();//audio timestamp + audioTimeQueue.pop(); + } + + if (this->m_iInternalReadCursor == this->m_iInternalWriteCursor) + { + if (this->m_bComplete) + bComplete = TRUE; + } + else + SetEvent(this->m_evtDataAvailable); + + LeaveCriticalSection(&this->m_csCircleBuffer); + } + + *pbBytesRead = iBytesRead; + return bComplete ? FALSE : TRUE; +} + diff --git a/windows/APIExample/APIExample/DirectShow/CircleBuffer.hpp b/windows/APIExample/APIExample/DirectShow/CircleBuffer.hpp new file mode 100644 index 000000000..184bfb1c7 --- /dev/null +++ b/windows/APIExample/APIExample/DirectShow/CircleBuffer.hpp @@ -0,0 +1,34 @@ +#pragma once +#include +#include +#define CIC_WAITTIMEOUT 0 +#define AUDIO_CALLBACK_TIMES 100 +#define MAX_AUDIO_SAMPLE_SIZE (48000*2*2/AUDIO_CALLBACK_TIMES)//sampleRate*sizeof(16bit)*channel+ AUDIO_CALLBACK_TIMES*sizeof(timestamp)=max_s +class CircleBuffer +{ +private: + BYTE* m_pBuffer; + unsigned int m_iBufferSize; + unsigned int m_iInternalReadCursor; + unsigned int m_iInternalWriteCursor; + HANDLE m_evtDataAvailable; + CRITICAL_SECTION m_csCircleBuffer; + BOOL m_bComplete; + int wait_timeout; + std::queue audioTimeQueue; + static CircleBuffer* m_pCircleBuffer; +public: + CircleBuffer(const unsigned int iBufferSize,int waittimeout); + ~CircleBuffer(void); + BOOL IsComplete(); + void SetComplete(); + unsigned int getFreeSize(); + unsigned int getUsedSize(); + void writeBuffer(const void* pSourceBuffer, const unsigned int iNumBytes, int audioTime); + BOOL readBuffer(void* pDestBuffer, const unsigned int iBytesToRead, unsigned int* pbBytesRead, int& audioTime); + void flushBuffer(); + + static CircleBuffer* GetInstance(); + static void CloseInstance(); +}; + diff --git a/windows/APIExample/APIExample/DirectShow/DShowHelper.cpp b/windows/APIExample/APIExample/DirectShow/DShowHelper.cpp new file mode 100644 index 000000000..80321b061 --- /dev/null +++ b/windows/APIExample/APIExample/DirectShow/DShowHelper.cpp @@ -0,0 +1,355 @@ +#include "../stdafx.h" +#include "DShowHelper.h" +#include +#include +#include "amvideo.h" +#include +CDShowHelper::CDShowHelper() +{ +} + + +CDShowHelper::~CDShowHelper() +{ +} + + +bool CDShowHelper::PinConfigHasMajorType(IPin *pin, const GUID &type) +{ + HRESULT hr; + IAMStreamConfig* config = nullptr; + int count = 0, size = 0; + + hr = pin->QueryInterface(IID_IAMStreamConfig, (void**)&config); + if (FAILED(hr)) + return false; + + hr = config->GetNumberOfCapabilities(&count, &size); + if (FAILED(hr)) + return false; + + std::vector caps; + caps.resize(size); + + for (int i = 0; i < count; i++) { + AM_MEDIA_TYPE* mt; + if (SUCCEEDED(config->GetStreamCaps(i, &mt, caps.data()))) + if (mt->majortype == type) + return true; + } + + return false; +} + +bool CDShowHelper::PinHasMajorType(IPin *pin, const GUID &type) +{ + HRESULT hr; + AM_MEDIA_TYPE* mt = nullptr; + IEnumMediaTypes* mediaEnum = nullptr; + + /* first, check the config caps. */ + if (PinConfigHasMajorType(pin, type)) + return true; + + /* then let's check the media type for the pin */ + if (FAILED(pin->EnumMediaTypes(&mediaEnum))) + return false; + + ULONG curVal = 0; + hr = mediaEnum->Next(1, &mt, &curVal); + if (hr != S_OK) + return false; + + return mt->majortype == type; +} + +bool CDShowHelper::PinIsDirection(IPin *pin, PIN_DIRECTION dir) +{ + if (!pin) + return false; + + PIN_DIRECTION pinDir; + return SUCCEEDED(pin->QueryDirection(&pinDir)) && pinDir == dir; +} + +HRESULT CDShowHelper::GetPinCategory(IPin *pin, GUID &category) +{ + if (!pin) + return E_POINTER; + IKsPropertySet* propertySet = nullptr; + pin->QueryInterface(_uuidof(IKsPropertySet), (void**)&propertySet); + DWORD size = 0; + if (propertySet == NULL) + return E_NOINTERFACE; + + return propertySet->Get(AMPROPSETID_Pin, AMPROPERTY_PIN_CATEGORY, + NULL, 0, &category, sizeof(GUID), &size); +} + +bool CDShowHelper::PinIsCategory(IPin *pin, const GUID &category) +{ + if (!pin) return false; + + GUID pinCategory; + HRESULT hr = GetPinCategory(pin, pinCategory); + + /* if the pin has no category interface, chances are we created it */ + if (FAILED(hr)) + return (hr == E_NOINTERFACE); + + return category == pinCategory; +} + +bool CDShowHelper::PinMatches(IPin *pin, const GUID &type, const GUID &category, + PIN_DIRECTION &dir) +{ + if (!PinHasMajorType(pin, type)) + return false; + if (!PinIsDirection(pin, dir)) + return false; + if (!PinIsCategory(pin, category)) + return false; + + return true; +} + +BOOL CDShowHelper::GetFilterPin(IBaseFilter *filter, const GUID &type, const GUID &category, + PIN_DIRECTION dir, IPin **pin) +{ + IPin* curPin = nullptr; + IEnumPins* pinsEnum = nullptr; + ULONG num = 0; + + if (!filter) + return false; + if (FAILED(filter->EnumPins(&pinsEnum))) + return false; + + while (pinsEnum->Next(1, &curPin, &num) == S_OK) { + + if (PinMatches(curPin, type, category, dir)) { + *pin = curPin; + (*pin)->AddRef(); + return true; + } + } + return false; +} + +bool CDShowHelper::PinNameIs(IPin *pin, const wchar_t *name) +{ + if (!pin) return false; + if (!name) return true; + + PIN_INFO pinInfo; + + if (FAILED(pin->QueryPinInfo(&pinInfo))) + return false; + + if (pinInfo.pFilter) + pinInfo.pFilter->Release(); + + return wcscmp(name, pinInfo.achName) == 0; +} + +bool CDShowHelper::GetPinByName(IBaseFilter *filter, PIN_DIRECTION dir, const wchar_t *name, + IPin **pin) +{ + CComPtr curPin = nullptr; + CComPtr pinsEnum = nullptr; + ULONG num = 0; + + if (!filter) + return false; + if (FAILED(filter->EnumPins(&pinsEnum))) + return false; + + while (pinsEnum->Next(1, &curPin, &num) == S_OK) { + + if (PinIsDirection(curPin, dir) && PinNameIs(curPin, name)) { + *pin = curPin.Detach(); + return true; + } + } + + return false; +} + + + +void CDShowHelper::FreeMediaType(AM_MEDIA_TYPE *lpAMMediaType) +{ + if (lpAMMediaType == NULL) + return; + + if (lpAMMediaType->cbFormat != 0) { + ::CoTaskMemFree((PVOID)lpAMMediaType->pbFormat); + lpAMMediaType->cbFormat = 0; + lpAMMediaType->pbFormat = NULL; + } + + if (lpAMMediaType->pUnk != NULL) { + lpAMMediaType->pUnk->Release(); + lpAMMediaType->pUnk = NULL; + } + + ::CoTaskMemFree(lpAMMediaType); +} + + +HRESULT CDShowHelper::CopyMediaType(AM_MEDIA_TYPE *pmtTarget, const AM_MEDIA_TYPE *pmtSource) +{ + if (!pmtSource || !pmtTarget) + return S_FALSE; + + *pmtTarget = *pmtSource; + + if (pmtSource->cbFormat && pmtSource->pbFormat) { + pmtTarget->pbFormat = + (PBYTE)CoTaskMemAlloc(pmtSource->cbFormat); + + if (pmtTarget->pbFormat == nullptr) { + pmtTarget->cbFormat = 0; + return E_OUTOFMEMORY; + } + else { + memcpy(pmtTarget->pbFormat, pmtSource->pbFormat, + pmtTarget->cbFormat); + } + } + + if (pmtTarget->pUnk != nullptr) + pmtTarget->pUnk->AddRef(); + + return S_OK; +} + + +BITMAPINFOHEADER *CDShowHelper::GetBitmapInfoHeader(AM_MEDIA_TYPE &mt) +{ + if (mt.formattype == FORMAT_VideoInfo) { + VIDEOINFOHEADER *vih; + vih = reinterpret_cast(mt.pbFormat); + return &vih->bmiHeader; + + } + else if (mt.formattype == FORMAT_VideoInfo2) { + VIDEOINFOHEADER2 *vih; + vih = reinterpret_cast(mt.pbFormat); + return &vih->bmiHeader; + } + + return NULL; +} + +bool CDShowHelper::EnumDevice(const GUID &type, IMoniker *deviceInfo, + IBaseFilter** outfilter, const wchar_t *name, const wchar_t *path) +{ + IPropertyBag* propertyData = nullptr; + IBaseFilter* filter = nullptr; + HRESULT hr; + + hr = deviceInfo->BindToStorage(0, 0, IID_IPropertyBag, + (void**)&propertyData); + if (FAILED(hr)) + return true; + + VARIANT deviceName, devicePath; + deviceName.vt = VT_BSTR; + devicePath.vt = VT_BSTR; + devicePath.bstrVal = NULL; + + hr = propertyData->Read(L"FriendlyName", &deviceName, NULL); + if (FAILED(hr)) + return true; + + + propertyData->Read(L"DevicePath", &devicePath, NULL); + + hr = deviceInfo->BindToObject(NULL, 0, IID_IBaseFilter, + (void**)&filter); + if (SUCCEEDED(hr)) { + if (deviceName.bstrVal && name && wcscmp(name, deviceName.bstrVal) != 0) + return true; + + if (devicePath.bstrVal && path && + wcscmp(path, devicePath.bstrVal) != 0) + return true; + + *outfilter = filter; + return false; + } + + return true; +} + +bool CDShowHelper::EnumDevices(const GUID &type, IBaseFilter** outFilter, const wchar_t *name, const wchar_t *path) +{ + //std::lock_guardCreateClassEnumerator(type, &enumMoniker, 0); + if (FAILED(hr)) { + OutputDebugString(L"EnumDevices: CreateClassEnumerator failed"); + return false; + } + + if (hr == S_OK) { + while (enumMoniker->Next(1, &deviceInfo, &count) == S_OK) { + if (!EnumDevice(type, deviceInfo, outFilter, name, path)) + //*outFilter = filter; + return true; + } + } + return true; +} + +bool CDShowHelper::GetDeviceFilter(const IID &type, const wchar_t *name, const wchar_t *path, + IBaseFilter **out) +{ + IBaseFilter* filter = nullptr; + CComPtr deviceEnum = nullptr; + CComPtr enumMoniker = nullptr; + CComPtr deviceInfo = nullptr; + HRESULT hr; + DWORD count = 0; + + hr = CoCreateInstance(CLSID_SystemDeviceEnum, NULL, + CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, (void**)&deviceEnum); + if (FAILED(hr)) { + OutputDebugString(L"Could not create ICreateDeviceEnum"); + return false; + } + + hr = deviceEnum->CreateClassEnumerator(type, &enumMoniker, 0); + if (FAILED(hr)) { + OutputDebugString(L"CreateClassEnumerator failed"); + return false; + } + + if (hr == S_OK) { + while (enumMoniker->Next(1, &deviceInfo, &count) == S_OK) { + if (!EnumDevice(type, deviceInfo, &filter, name, path)) { + if (filter != nullptr) { + *out = filter; + return true; + } + } + deviceInfo.Release(); + } + } + return false; +} \ No newline at end of file diff --git a/windows/APIExample/APIExample/DirectShow/DShowHelper.h b/windows/APIExample/APIExample/DirectShow/DShowHelper.h new file mode 100644 index 000000000..015ec1a1d --- /dev/null +++ b/windows/APIExample/APIExample/DirectShow/DShowHelper.h @@ -0,0 +1,33 @@ +#pragma once +#include +class CDShowHelper +{ +public: + CDShowHelper(); + ~CDShowHelper(); + + static bool PinMatches(IPin *pin, const GUID &type, const GUID &category, + PIN_DIRECTION &dir); + static bool PinHasMajorType(IPin *pin, const GUID &type); + static bool PinConfigHasMajorType(IPin *pin, const GUID &type); + static bool PinIsDirection(IPin *pin, PIN_DIRECTION dir); + static bool PinIsCategory(IPin *pin, const GUID &category); + static HRESULT GetPinCategory(IPin *pin, GUID &category); + static bool GetPinByName(IBaseFilter *filter, PIN_DIRECTION dir, const wchar_t *name, + IPin **pin); + static bool PinNameIs(IPin *pin, const wchar_t *name); + static BOOL GetFilterPin(IBaseFilter *filter, const GUID &type, const GUID &category, + PIN_DIRECTION dir, IPin **pin); + + static void FreeMediaType(AM_MEDIA_TYPE *lpAMMediaType); + static HRESULT CopyMediaType(AM_MEDIA_TYPE *pmtTarget, const AM_MEDIA_TYPE *pmtSource); + static BITMAPINFOHEADER *GetBitmapInfoHeader(AM_MEDIA_TYPE &mt); + static bool GetDeviceFilter(const IID &type, const wchar_t *name, const wchar_t *path, + IBaseFilter **filter); + static bool EnumDevice(const GUID &type, IMoniker *deviceInfo, + IBaseFilter** filter, const wchar_t *name, const wchar_t *path); + static bool EnumDevices(const GUID &type, IBaseFilter** filter, const wchar_t *name, const wchar_t *path); + + +}; + diff --git a/windows/APIExample/APIExample/DirectShow/IAGDShowDevice.h b/windows/APIExample/APIExample/DirectShow/IAGDShowDevice.h new file mode 100644 index 000000000..f92222edc --- /dev/null +++ b/windows/APIExample/APIExample/DirectShow/IAGDShowDevice.h @@ -0,0 +1,55 @@ +#pragma once + +#include + + +#include +#include + +#pragma comment(lib, "strmbase.lib") + + +typedef struct _AGORA_DEVICE_INFO +{ + GUID guidDevice; + TCHAR szDeviceName[MAX_PATH]; + TCHAR szDevicePath[MAX_PATH]; + TCHAR szDeviceDesc[MAX_PATH]; + +} AGORA_DEVICE_INFO, *PAGORA_DEVICE_INFO, *LPAGORA_DEVICE_INFO; + +enum { + DEVICE_START = 0, // start capture + DEVICE_STOP, // stop capture +}; + +class IDShowCaptureDevice +{ +public: + + virtual BOOL Create() = 0; + virtual void Close() = 0; + + virtual BOOL EnumDeviceList() = 0; + virtual int GetDeviceCount() = 0; + virtual BOOL GetDeviceInfo(int nIndex, LPAGORA_DEVICE_INFO lpDeviceInfo) = 0; + + virtual BOOL OpenDevice(int nIndex) = 0; + + virtual int GetCurrentDevice(LPTSTR lpDevicePath, SIZE_T *nDevicePathLen) = 0; + virtual void CloseDevice() = 0; + + virtual int GetMediaCapCount() = 0; + virtual BOOL GetMediaCap(int nIndex, AM_MEDIA_TYPE **ppMediaType, LPVOID lpMediaStreamConfigCaps, SIZE_T nSize) = 0; + virtual BOOL SelectMediaCap(int nIndex) = 0; + virtual int GetSelectedMediaCap() = 0; + + virtual BOOL GetVideoCap(int nIndex, VIDEOINFOHEADER *lpVideoInfo) = 0; + virtual BOOL GetCurrentVideoCap(VIDEOINFOHEADER *lpVideoInfo) = 0; + virtual BOOL GetCurrentMediaType(AM_MEDIA_TYPE **pMediaType) = 0; + virtual BOOL CreateCaptureFilter() = 0; + virtual BOOL Start() = 0; + virtual void Stop() = 0; + virtual BOOL GetAudioCap(int nIndex, WAVEFORMATEX *lpWaveInfo) = 0; + virtual BOOL GetCurrentAudioCap(WAVEFORMATEX *lpWaveInfo) = 0; +}; \ No newline at end of file diff --git a/windows/APIExample/APIExample/DirectShow/capture-filter.cpp b/windows/APIExample/APIExample/DirectShow/capture-filter.cpp new file mode 100644 index 000000000..75c19f674 --- /dev/null +++ b/windows/APIExample/APIExample/DirectShow/capture-filter.cpp @@ -0,0 +1,686 @@ +/* + * Copyright (C) 2014 Hugh Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ +#include "capture-filter.hpp" +#include "DShowHelper.h" +//namespace DShow { + +#if 0 +#define PrintFunc(x) Debug(x) +#else +#define PrintFunc(x) +#endif + + +#define FILTER_NAME L"Capture Filter" +#define VIDEO_PIN_NAME L"Video Capture" +#define AUDIO_PIN_NAME L"Audio Capture" + +CapturePin::CapturePin(CaptureFilter *filter_, const PinCaptureInfo &info) + : refCount (0), + captureInfo (info), + filter (filter_) +{ + memset(&connectedMediaType, 0, sizeof(AM_MEDIA_TYPE)); + connectedMediaType.majortype = info.expectedMajorType; +} + +CapturePin::~CapturePin() +{ + +} + +STDMETHODIMP CapturePin::QueryInterface(REFIID riid, void **ppv) +{ + if (riid == IID_IUnknown) { + AddRef(); + *ppv = this; + } else if (riid == IID_IPin) { + AddRef(); + *ppv = (IPin*)this; + } else if (riid == IID_IMemInputPin) { + AddRef(); + *ppv = (IMemInputPin*)this; + } else { + *ppv = nullptr; + return E_NOINTERFACE; + } + + return NOERROR; +} + +STDMETHODIMP_(ULONG) CapturePin::AddRef() +{ + return (ULONG)InterlockedIncrement(&refCount); +} + +STDMETHODIMP_(ULONG) CapturePin::Release() +{ + if (!InterlockedDecrement(&refCount)) { + delete this; + return 0; + } + + return (ULONG)refCount; +} + +// IPin methods +STDMETHODIMP CapturePin::Connect(IPin *pReceivePin, const AM_MEDIA_TYPE *pmt) +{ + PrintFunc(L"CapturePin::Connect"); + + if (filter->state == State_Running) + return VFW_E_NOT_STOPPED; + + if (connectedPin) + return VFW_E_ALREADY_CONNECTED; + + if (!pmt) + return S_OK; + + if (pmt->majortype != GUID_NULL && + pmt->majortype != captureInfo.expectedMajorType) + return S_FALSE; + + if (pmt->majortype == captureInfo.expectedMajorType && + !IsValidMediaType(pmt)) + return S_FALSE; + + ////DSHOW_UNUSED(pReceivePin); + return S_OK; +} + +STDMETHODIMP CapturePin::ReceiveConnection(IPin *pConnector, + const AM_MEDIA_TYPE *pmt) +{ + PrintFunc(L"CapturePin::ReceiveConnection"); + + if (filter->state != State_Stopped) + return VFW_E_NOT_STOPPED; + if (!pConnector || !pmt) + return E_POINTER; + if (connectedPin) + return VFW_E_ALREADY_CONNECTED; + + if (QueryAccept(pmt) != S_OK) + return VFW_E_TYPE_NOT_ACCEPTED; + + connectedPin = pConnector; + connectedMediaType = *pmt; + + return S_OK; +} + +STDMETHODIMP CapturePin::Disconnect() +{ + PrintFunc(L"CapturePin::Disconnect"); + + if (!connectedPin) + return S_FALSE; + + connectedPin = nullptr; + return S_OK; +} + + +STDMETHODIMP CapturePin::ConnectedTo(IPin **pPin) +{ + PrintFunc(L"CapturePin::ConnectedTo"); + + if (!connectedPin) + return VFW_E_NOT_CONNECTED; + + IPin *pin = connectedPin; + pin->AddRef(); + *pPin = pin; + return S_OK; +} + +STDMETHODIMP CapturePin::ConnectionMediaType(AM_MEDIA_TYPE *pmt) +{ + PrintFunc(L"CapturePin::ConnectionMediaType"); + + if (!connectedPin) + return VFW_E_NOT_CONNECTED; + pmt = &connectedMediaType; + //return CopyMediaType(pmt, connectedMediaType); + return NOERROR; +} + +STDMETHODIMP CapturePin::QueryPinInfo(PIN_INFO *pInfo) +{ + PrintFunc(L"CapturePin::QueryPinInfo"); + + pInfo->pFilter = filter; + if (filter) { + IBaseFilter *ptr = filter; + ptr->AddRef(); + } + + if (captureInfo.expectedMajorType == MEDIATYPE_Video) + memcpy(pInfo->achName, VIDEO_PIN_NAME, sizeof(VIDEO_PIN_NAME)); + else + memcpy(pInfo->achName, AUDIO_PIN_NAME, sizeof(AUDIO_PIN_NAME)); + + pInfo->dir = PINDIR_INPUT; + + return NOERROR; +} + +STDMETHODIMP CapturePin::QueryDirection(PIN_DIRECTION *pPinDir) +{ + *pPinDir = PINDIR_INPUT; + return NOERROR; +} + +#define CAPTURE_PIN_NAME L"Capture Pin" + +STDMETHODIMP CapturePin::QueryId(LPWSTR *lpId) +{ + wchar_t *str = (wchar_t*)CoTaskMemAlloc(sizeof(CAPTURE_PIN_NAME)); + memcpy(str, CAPTURE_PIN_NAME, sizeof(CAPTURE_PIN_NAME)); + *lpId = str; + return S_OK; +} + +STDMETHODIMP CapturePin::QueryAccept(const AM_MEDIA_TYPE *pmt) +{ + PrintFunc(L"CapturePin::QueryAccept"); + + if (pmt->majortype != captureInfo.expectedMajorType) + return S_FALSE; + if (!IsValidMediaType(pmt)) + return S_FALSE; + + if (connectedPin) + connectedMediaType = *pmt; + + return S_OK; +} + +STDMETHODIMP CapturePin::EnumMediaTypes(IEnumMediaTypes **ppEnum) +{ + PrintFunc(L"CapturePin::EnumMediaTypes"); + + *ppEnum = new CaptureEnumMediaTypes(this); + if (!*ppEnum) + return E_OUTOFMEMORY; + + return NOERROR; +} + +STDMETHODIMP CapturePin::QueryInternalConnections(IPin **apPin, ULONG *nPin) +{ + PrintFunc(L"CapturePin::QueryInternalConnections"); + + return E_NOTIMPL; +} + +STDMETHODIMP CapturePin::EndOfStream() +{ + PrintFunc(L"CapturePin::EndOfStream"); + + return S_OK; +} + +STDMETHODIMP CapturePin::BeginFlush() +{ + PrintFunc(L"CapturePin::BeginFlush"); + + flushing = true; + return S_OK; +} + +STDMETHODIMP CapturePin::EndFlush() +{ + PrintFunc(L"CapturePin::EndFlush"); + + flushing = false; + return S_OK; +} + +STDMETHODIMP CapturePin::NewSegment(REFERENCE_TIME tStart, + REFERENCE_TIME tStop, double dRate) +{ + PrintFunc(L"CapturePin::NewSegment"); + return S_OK; +} + +// IMemInputPin methods +STDMETHODIMP CapturePin::GetAllocator(IMemAllocator **ppAllocator) +{ + PrintFunc(L"CapturePin::GetAllocator"); + + return VFW_E_NO_ALLOCATOR; +} + +STDMETHODIMP CapturePin::NotifyAllocator(IMemAllocator *pAllocator, + BOOL bReadOnly) +{ + PrintFunc(L"CapturePin::NotifyAllocator"); + + return S_OK; +} + +STDMETHODIMP CapturePin::GetAllocatorRequirements(ALLOCATOR_PROPERTIES *pProps) +{ + PrintFunc(L"CapturePin::GetAllocatorRequirements"); + + return E_NOTIMPL; +} + +STDMETHODIMP CapturePin::Receive(IMediaSample *pSample) +{ + PrintFunc(L"CapturePin::Receive"); + + if (flushing) + return S_FALSE; + + if (pSample) + captureInfo.callback(pSample); + + return S_OK; +} + +STDMETHODIMP CapturePin::ReceiveMultiple(IMediaSample **pSamples, + long nSamples, long *nSamplesProcessed) +{ + PrintFunc(L"CapturePin::ReceiveMultiple"); + + if (flushing) + return S_FALSE; + + for (long i = 0; i < nSamples; i++) + if (pSamples[i]) + captureInfo.callback(pSamples[i]); + + *nSamplesProcessed = nSamples; + + return S_OK; +} + +STDMETHODIMP CapturePin::ReceiveCanBlock() {return S_FALSE;} + +bool CapturePin::IsValidMediaType(const AM_MEDIA_TYPE *pmt) const +{ + if (pmt->pbFormat) { + if (pmt->subtype != captureInfo.expectedSubType || + pmt->majortype != captureInfo.expectedMajorType) + return false; + + if (captureInfo.expectedMajorType == MEDIATYPE_Video) { + AM_MEDIA_TYPE mt = *pmt; + const BITMAPINFOHEADER *bih = CDShowHelper::GetBitmapInfoHeader(mt); + if (!bih || bih->biHeight == 0 || bih->biWidth == 0) + return false; + } + } + + return true; +} + +// ============================================================================ + +class MiscFlagsHandler : public IAMFilterMiscFlags { + volatile long refCount = 0; + +public: + inline MiscFlagsHandler() {} + virtual ~MiscFlagsHandler() {} + + STDMETHODIMP QueryInterface(REFIID riid, void **ppv) + { + if (riid == IID_IUnknown) { + AddRef(); + *ppv = this; + } else { + *ppv = nullptr; + return E_NOINTERFACE; + } + + return NOERROR; + } + + STDMETHODIMP_(ULONG) AddRef() + { + return InterlockedIncrement(&refCount); + } + + STDMETHODIMP_(ULONG) Release() + { + if (!InterlockedDecrement(&refCount)) { + delete this; + return 0; + } + + return refCount; + } + + STDMETHODIMP_(ULONG) GetMiscFlags() + { + return AM_FILTER_MISC_FLAGS_IS_RENDERER; + } +}; + +CaptureFilter::CaptureFilter(const PinCaptureInfo &info) + : refCount (0), + state (State_Stopped), + pin (new CapturePin(this, info)), + misc (new MiscFlagsHandler) +{ +} + +CaptureFilter::~CaptureFilter() +{ +} + +// IUnknown methods +STDMETHODIMP CaptureFilter::QueryInterface(REFIID riid, void **ppv) +{ + if (riid == IID_IUnknown) { + AddRef(); + *ppv = this; + } else if (riid == IID_IPersist) { + AddRef(); + *ppv = (IPersist*)this; + } else if (riid == IID_IMediaFilter) { + AddRef(); + *ppv = (IMediaFilter*)this; + } else if (riid == IID_IBaseFilter) { + AddRef(); + *ppv = (IBaseFilter*)this; + } else if (riid == IID_IAMFilterMiscFlags) { + + misc.CopyTo((IAMFilterMiscFlags**)ppv); + } else { + *ppv = nullptr; + return E_NOINTERFACE; + } + + return NOERROR; +} + +STDMETHODIMP_(ULONG) CaptureFilter::AddRef() +{ + return InterlockedIncrement(&refCount); +} + +STDMETHODIMP_(ULONG) CaptureFilter::Release() +{ + if (!InterlockedDecrement(&refCount)) { + delete this; + return 0; + } + + return refCount; +} + +// IPersist method +STDMETHODIMP CaptureFilter::GetClassID(CLSID *pClsID) +{ + return E_NOTIMPL; +} + +// IMediaFilter methods +STDMETHODIMP CaptureFilter::GetState(DWORD dwMSecs, FILTER_STATE *State) +{ + PrintFunc(L"CaptureFilter::GetState"); + + *State = state; + + return S_OK; +} + +STDMETHODIMP CaptureFilter::SetSyncSource(IReferenceClock *pClock) +{ + return S_OK; +} + +STDMETHODIMP CaptureFilter::GetSyncSource(IReferenceClock **pClock) +{ + *pClock = nullptr; + return NOERROR; +} + +STDMETHODIMP CaptureFilter::Stop() +{ + PrintFunc(L"CaptureFilter::Stop"); + + state = State_Stopped; + return S_OK; +} + +STDMETHODIMP CaptureFilter::Pause() +{ + PrintFunc(L"CaptureFilter::Pause"); + + state = State_Paused; + return S_OK; +} + +STDMETHODIMP CaptureFilter::Run(REFERENCE_TIME tStart) +{ + PrintFunc(L"CaptureFilter::Run"); + + state = State_Running; + + return S_OK; +} + +// IBaseFilter methods +STDMETHODIMP CaptureFilter::EnumPins(IEnumPins **ppEnum) +{ + PrintFunc(L"CaptureFilter::EnumPins"); + + *ppEnum = new CaptureEnumPins(this, nullptr); + return (*ppEnum == nullptr) ? E_OUTOFMEMORY : NOERROR; +} + +STDMETHODIMP CaptureFilter::FindPin(LPCWSTR Id, IPin **ppPin) +{ + PrintFunc(L"CaptureFilter::FindPin"); + + return E_NOTIMPL; +} + +STDMETHODIMP CaptureFilter::QueryFilterInfo(FILTER_INFO *pInfo) +{ + PrintFunc(L"CaptureFilter::QueryFilterInfo"); + + memcpy(pInfo->achName, FILTER_NAME, sizeof(FILTER_NAME)); + + pInfo->pGraph = graph; + if (graph) { + IFilterGraph *graph_ptr = graph; + graph_ptr->AddRef(); + } + return NOERROR; +} + +STDMETHODIMP CaptureFilter::JoinFilterGraph(IFilterGraph *pGraph, LPCWSTR pName) +{ + graph = pGraph; + return NOERROR; +} + +STDMETHODIMP CaptureFilter::QueryVendorInfo(LPWSTR *pVendorInfo) +{ + return E_NOTIMPL; +} + +// ============================================================================ + +CaptureEnumPins::CaptureEnumPins(CaptureFilter *filter_, CaptureEnumPins *pEnum) + : filter (filter_) +{ + curPin = (pEnum != nullptr) ? pEnum->curPin : 0; +} + +CaptureEnumPins::~CaptureEnumPins() +{ +} + +// IUnknown +STDMETHODIMP CaptureEnumPins::QueryInterface(REFIID riid, void **ppv) +{ + if (riid == IID_IUnknown || riid == IID_IEnumPins) { + AddRef(); + *ppv = (IEnumPins*)this; + return NOERROR; + } else { + *ppv = nullptr; + return E_NOINTERFACE; + } +} + +STDMETHODIMP_(ULONG) CaptureEnumPins::AddRef() +{ + return (ULONG)InterlockedIncrement(&refCount); +} + +STDMETHODIMP_(ULONG) CaptureEnumPins::Release() +{ + if (!InterlockedDecrement(&refCount)) { + delete this; + return 0; + } + + return (ULONG)refCount; +} + +// IEnumPins +STDMETHODIMP CaptureEnumPins::Next(ULONG cPins, IPin **ppPins, ULONG *pcFetched) +{ + UINT nFetched = 0; + + if (curPin == 0 && cPins > 0) { + IPin *pPin = filter->GetPin(); + + *ppPins = pPin; + pPin->AddRef(); + + nFetched = 1; + curPin++; + } + + if (pcFetched) *pcFetched = nFetched; + + return (nFetched == cPins) ? S_OK : S_FALSE; +} + +STDMETHODIMP CaptureEnumPins::Skip(ULONG cPins) +{ + return ((curPin += cPins) > 1) ? S_FALSE : S_OK; +} + +STDMETHODIMP CaptureEnumPins::Reset() +{ + curPin = 0; + return S_OK; +} + +STDMETHODIMP CaptureEnumPins::Clone(IEnumPins **ppEnum) +{ + *ppEnum = new CaptureEnumPins(filter, this); + return (*ppEnum == nullptr) ? E_OUTOFMEMORY : NOERROR; +} + + +// ============================================================================ + +CaptureEnumMediaTypes::CaptureEnumMediaTypes(CapturePin *pin_) + : pin (pin_) +{ +} + +CaptureEnumMediaTypes::~CaptureEnumMediaTypes() +{ +} + +STDMETHODIMP CaptureEnumMediaTypes::QueryInterface(REFIID riid, void **ppv) +{ + if (riid == IID_IUnknown || riid == IID_IEnumMediaTypes) { + AddRef(); + *ppv = this; + return NOERROR; + } else { + *ppv = nullptr; + return E_NOINTERFACE; + } +} + +STDMETHODIMP_(ULONG) CaptureEnumMediaTypes::AddRef() +{ + return (ULONG)InterlockedIncrement(&refCount); +} + +STDMETHODIMP_(ULONG) CaptureEnumMediaTypes::Release() +{ + if (!InterlockedDecrement(&refCount)) { + delete this; + return 0; + } + + return (ULONG)refCount; +} + +// IEnumMediaTypes +STDMETHODIMP CaptureEnumMediaTypes::Next(ULONG cMediaTypes, + AM_MEDIA_TYPE **ppMediaTypes, ULONG *pcFetched) +{ + PrintFunc(L"CaptureEnumMediaTypes::Next"); + + UINT nFetched = 0; + + if (curMT == 0 && cMediaTypes > 0) { + AM_MEDIA_TYPE *ptr = (AM_MEDIA_TYPE *)CoTaskMemAlloc(sizeof(*ptr)); + memset(ptr, 0, sizeof(*ptr)); + CDShowHelper::CopyMediaType(ptr, &pin->connectedMediaType); + *ppMediaTypes = ptr; + //CDShowHelper::CopyMediaType(&pin->connectedMediaType, *ppMediaTypes); + nFetched = 1; + curMT++; + } + + if (pcFetched) *pcFetched = nFetched; + + return (nFetched == cMediaTypes) ? S_OK : S_FALSE; +} + +STDMETHODIMP CaptureEnumMediaTypes::Skip(ULONG cMediaTypes) +{ + PrintFunc(L"CaptureEnumMediaTypes::Skip"); + + return ((curMT += cMediaTypes) > 1) ? S_FALSE : S_OK; +} + +STDMETHODIMP CaptureEnumMediaTypes::Reset() +{ + PrintFunc(L"CaptureEnumMediaTypes::Reset"); + + curMT = 0; + return S_OK; +} + +STDMETHODIMP CaptureEnumMediaTypes::Clone(IEnumMediaTypes **ppEnum) +{ + *ppEnum = new CaptureEnumMediaTypes(pin); + return (*ppEnum == nullptr) ? E_OUTOFMEMORY : NOERROR; +} + +//}; /* namespace DShow */ diff --git a/windows/APIExample/APIExample/DirectShow/capture-filter.hpp b/windows/APIExample/APIExample/DirectShow/capture-filter.hpp new file mode 100644 index 000000000..23e96beb1 --- /dev/null +++ b/windows/APIExample/APIExample/DirectShow/capture-filter.hpp @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2014 Hugh Bailey + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 + * USA + */ + +#pragma once +#include +#include +#include +#include +#include +//namespace DShow { + +class CaptureFilter; +class CaptureSource; + +typedef void (*CaptureCallback)(void *param, IMediaSample *sample); + +struct PinCaptureInfo { + std::function callback; + GUID expectedMajorType; + GUID expectedSubType; +}; + +class CapturePin : public IPin, public IMemInputPin { + friend class CaptureEnumMediaTypes; + + volatile long refCount; + + PinCaptureInfo captureInfo; + CComPtr connectedPin = nullptr; + CComPtr filter = nullptr; + AM_MEDIA_TYPE connectedMediaType; + volatile bool flushing = false; + + bool IsValidMediaType(const AM_MEDIA_TYPE *pmt) const; + +public: + CapturePin(CaptureFilter *filter, const PinCaptureInfo &info); + virtual ~CapturePin(); + + STDMETHODIMP QueryInterface(REFIID riid, void **ppv); + STDMETHODIMP_(ULONG) AddRef(); + STDMETHODIMP_(ULONG) Release(); + + // IPin methods + STDMETHODIMP Connect(IPin *pReceivePin, const AM_MEDIA_TYPE *pmt); + STDMETHODIMP ReceiveConnection(IPin *connector, + const AM_MEDIA_TYPE *pmt); + STDMETHODIMP Disconnect(); + STDMETHODIMP ConnectedTo(IPin **pPin); + STDMETHODIMP ConnectionMediaType(AM_MEDIA_TYPE *pmt); + STDMETHODIMP QueryPinInfo(PIN_INFO *pInfo); + STDMETHODIMP QueryDirection(PIN_DIRECTION *pPinDir); + STDMETHODIMP QueryId(LPWSTR *lpId); + STDMETHODIMP QueryAccept(const AM_MEDIA_TYPE *pmt); + STDMETHODIMP EnumMediaTypes(IEnumMediaTypes **ppEnum); + STDMETHODIMP QueryInternalConnections(IPin* *apPin, ULONG *nPin); + STDMETHODIMP EndOfStream(); + + STDMETHODIMP BeginFlush(); + STDMETHODIMP EndFlush(); + STDMETHODIMP NewSegment(REFERENCE_TIME tStart, REFERENCE_TIME tStop, + double dRate); + + // IMemInputPin methods + STDMETHODIMP GetAllocator(IMemAllocator **ppAllocator); + STDMETHODIMP NotifyAllocator(IMemAllocator *pAllocator, BOOL bReadOnly); + STDMETHODIMP GetAllocatorRequirements(ALLOCATOR_PROPERTIES *pProps); + STDMETHODIMP Receive(IMediaSample *pSample); + STDMETHODIMP ReceiveMultiple(IMediaSample **pSamples, long nSamples, + long *nSamplesProcessed); + STDMETHODIMP ReceiveCanBlock(); +}; + +class CaptureFilter : public IBaseFilter { + friend class CapturePin; + + volatile long refCount; + FILTER_STATE state; + CComPtr graph = nullptr; + CComPtr pin = nullptr; + + CComPtr misc; + +public: + CaptureFilter(const PinCaptureInfo &info); + virtual ~CaptureFilter(); + + // IUnknown methods + STDMETHODIMP QueryInterface(REFIID riid, void **ppv); + STDMETHODIMP_(ULONG) AddRef(); + STDMETHODIMP_(ULONG) Release(); + + // IPersist method + STDMETHODIMP GetClassID(CLSID *pClsID); + + // IMediaFilter methods + STDMETHODIMP GetState(DWORD dwMSecs, FILTER_STATE *State); + STDMETHODIMP SetSyncSource(IReferenceClock *pClock); + STDMETHODIMP GetSyncSource(IReferenceClock **pClock); + STDMETHODIMP Stop(); + STDMETHODIMP Pause(); + STDMETHODIMP Run(REFERENCE_TIME tStart); + + // IBaseFilter methods + STDMETHODIMP EnumPins(IEnumPins **ppEnum); + STDMETHODIMP FindPin(LPCWSTR Id, IPin **ppPin); + STDMETHODIMP QueryFilterInfo(FILTER_INFO *pInfo); + STDMETHODIMP JoinFilterGraph(IFilterGraph *pGraph, LPCWSTR pName); + STDMETHODIMP QueryVendorInfo(LPWSTR *pVendorInfo); + + inline CapturePin* GetPin() const {return (CapturePin*)pin;} +}; + +class CaptureEnumPins : public IEnumPins { + volatile long refCount = 1; + + CComPtr filter; + UINT curPin; + +public: + CaptureEnumPins(CaptureFilter *filter, CaptureEnumPins *pEnum); + virtual ~CaptureEnumPins(); + + // IUnknown + STDMETHODIMP QueryInterface(REFIID riid, void **ppv); + STDMETHODIMP_(ULONG) AddRef(); + STDMETHODIMP_(ULONG) Release(); + + // IEnumPins + STDMETHODIMP Next(ULONG cPins, IPin **ppPins, ULONG *pcFetched); + STDMETHODIMP Skip(ULONG cPins); + STDMETHODIMP Reset(); + STDMETHODIMP Clone(IEnumPins **ppEnum); +}; + +class CaptureEnumMediaTypes : public IEnumMediaTypes { + volatile long refCount = 1; + CComPtr pin = nullptr; + + UINT curMT = 0; + +public: + CaptureEnumMediaTypes(CapturePin *pin); + virtual ~CaptureEnumMediaTypes(); + + // IUnknown + STDMETHODIMP QueryInterface(REFIID riid, void **ppv); + STDMETHODIMP_(ULONG) AddRef(); + STDMETHODIMP_(ULONG) Release(); + + // IEnumMediaTypes + STDMETHODIMP Next(ULONG cMediaTypes, AM_MEDIA_TYPE **ppMediaTypes, + ULONG *pcFetched); + STDMETHODIMP Skip(ULONG cMediaTypes); + STDMETHODIMP Reset(); + STDMETHODIMP Clone(IEnumMediaTypes **ppEnum); +}; + +//}; /* namespace DShow */ diff --git a/windows/APIExample/APIExample/Language.h b/windows/APIExample/APIExample/Language.h new file mode 100644 index 000000000..5ac529d19 --- /dev/null +++ b/windows/APIExample/APIExample/Language.h @@ -0,0 +1,293 @@ +#pragma once +#define INFO_LEN 255 +//main dialog +extern wchar_t commonGroupDoc[INFO_LEN]; +extern wchar_t commonDocumentWebsite[INFO_LEN]; +extern wchar_t commonFAQWebsite[INFO_LEN]; +extern wchar_t commonRegisterWebsite[INFO_LEN]; +extern wchar_t commonDemoWebsite[INFO_LEN]; + +extern wchar_t commonBasicScene[INFO_LEN]; +extern wchar_t commonAdvanceScene[INFO_LEN]; +//agora enum +extern wchar_t agoraRoleBroadcaster[INFO_LEN]; +extern wchar_t agoraRoleAudience[INFO_LEN]; + + +//common ctrl +extern wchar_t commonCtrlChannel[INFO_LEN]; +extern wchar_t commonCtrlJoinChannel[INFO_LEN]; +extern wchar_t commonCtrlLeaveChannel[INFO_LEN]; +extern wchar_t commonCtrlClientRole[INFO_LEN]; +//scene list +extern wchar_t basicLiveBroadcasting[INFO_LEN]; +extern wchar_t advancedRtmpInject[INFO_LEN]; +extern wchar_t advancedRtmpStreaming[INFO_LEN]; +extern wchar_t advancedVideoMetadata[INFO_LEN]; +extern wchar_t advancedCustomEncrypt[INFO_LEN]; +extern wchar_t advancedMediaEncrypt[INFO_LEN]; +extern wchar_t advancedScreenCap[INFO_LEN]; +extern wchar_t advancedVideoProfile[INFO_LEN]; +extern wchar_t advancedAudioProfile[INFO_LEN]; +extern wchar_t advancedAudioMixing[INFO_LEN]; +extern wchar_t advancedBeauty[INFO_LEN]; +extern wchar_t advancedBeautyAudio[INFO_LEN]; +extern wchar_t advancedCustomVideoCapture[INFO_LEN]; +extern wchar_t advancedMediaIOCustomVideoCapture[INFO_LEN]; +extern wchar_t advancedOriginalVideo[INFO_LEN]; +extern wchar_t advancedMediaAudioCapture[INFO_LEN]; +extern wchar_t advancedCustomAudioCapture[INFO_LEN]; +extern wchar_t advancedOriginalAudio[INFO_LEN]; +extern wchar_t advancedMediaPlayer[INFO_LEN]; +extern wchar_t advancedAudioEffect[INFO_LEN]; +extern wchar_t advancedMultiChannel[INFO_LEN]; +extern wchar_t advancedPerCallTest[INFO_LEN]; +extern wchar_t advancedAudioVolume[INFO_LEN]; +extern wchar_t advancedReportInCall[INFO_LEN]; +extern wchar_t advancedRegionConn[INFO_LEN]; +extern wchar_t advancedCrossChannel[INFO_LEN]; + + +//live broadcasting +extern wchar_t liveCtrlPersons[INFO_LEN]; +//rtmp streaming +extern wchar_t rtmpStreamingCtrlPublishUrl[INFO_LEN]; +extern wchar_t rtmpStreamingCtrlAdd[INFO_LEN]; +extern wchar_t rtmpStreamingCtrlRemove[INFO_LEN]; +extern wchar_t rtmpStreamingCtrlTransCoding[INFO_LEN]; +extern wchar_t rtmpStreamingCtrlRemoveAll[INFO_LEN]; +//rtmp Inject +extern wchar_t rtmpInjectCtrlUrl[INFO_LEN]; +extern wchar_t rtmpInjectCtrlInject[INFO_LEN]; +extern wchar_t rtmpInjectCtrlRemove[INFO_LEN]; +//rtmp stream state changed +extern wchar_t agoraRtmpStateIdle[INFO_LEN]; +extern wchar_t agoraRtmpStateConnecting[INFO_LEN]; +extern wchar_t agoraRtmpStateRunning[INFO_LEN]; +extern wchar_t agoraRtmpStateRunningSuccess[INFO_LEN]; +extern wchar_t agoraRtmpStateRecovering[INFO_LEN]; +extern wchar_t agoraRtmpStateInvalidArg[INFO_LEN]; +extern wchar_t agoraRtmpStateEncrypted[INFO_LEN]; +extern wchar_t agoraRtmpStateConnTimeout[INFO_LEN]; +extern wchar_t agoraRtmpStateInrealErr[INFO_LEN]; +extern wchar_t agoraRtmpStateServerErr[INFO_LEN]; +extern wchar_t agoraRtmpStateTooOften[INFO_LEN]; +extern wchar_t agoraRtmpStateReachLimit[INFO_LEN]; +extern wchar_t agoraRtmpStateNotAuth[INFO_LEN]; +extern wchar_t agoraRtmpStateNotFound[INFO_LEN]; +extern wchar_t agoraRtmpStateNotSupported[INFO_LEN]; + +// inject status +extern wchar_t agoraInjectStartSucc[INFO_LEN]; +extern wchar_t agoraInjectExist[INFO_LEN]; +extern wchar_t agoraInjectStartUnAuth[INFO_LEN]; +extern wchar_t agoraInjectStartTimeout[INFO_LEN]; +extern wchar_t agoraInjectStartFailed[INFO_LEN]; +extern wchar_t agoraInjectStopSuccess[INFO_LEN]; +extern wchar_t agoraInjectNotFound[INFO_LEN]; +extern wchar_t agoraInjectStopUnAuth[INFO_LEN]; + +extern wchar_t agoraInjectStopTimeout[INFO_LEN]; +extern wchar_t agoraInjectStopFailed[INFO_LEN]; +extern wchar_t agoraInjectBroken[INFO_LEN]; + +//video SEI +extern wchar_t videoSEIInformation[INFO_LEN]; +extern wchar_t metadataCtrlSendSEI[INFO_LEN]; +extern wchar_t metadataCtrlBtnSend[INFO_LEN]; +extern wchar_t metadataCtrlBtnClear[INFO_LEN]; + +//media encrypt +extern wchar_t mediaEncryptCtrlMode[INFO_LEN]; +extern wchar_t mediaEncryptCtrlSecret[INFO_LEN]; +extern wchar_t mediaEncryptCtrlSetEncrypt[INFO_LEN]; + +//custom encrypt +extern wchar_t customEncryptCtrlEncrypt[INFO_LEN]; +extern wchar_t customEncryptCtrlSetEncrypt[INFO_LEN]; +extern wchar_t customEncryptCtrlCancelEncrypt[INFO_LEN]; + +//screen share +extern wchar_t screenShareCtrlScreenCap[INFO_LEN]; +extern wchar_t screenShareCtrlStartCap[INFO_LEN]; +extern wchar_t screenShareCtrlEndCap[INFO_LEN]; + +extern wchar_t screenShareCtrlScreen[INFO_LEN]; +extern wchar_t screenShareCtrlShareSCreen[INFO_LEN]; +extern wchar_t screenShareCtrlStopShare[INFO_LEN]; +extern wchar_t screenShareCtrlGeneralSettings[INFO_LEN]; +extern wchar_t screenShareCtrlFPS[INFO_LEN]; +extern wchar_t screenShareCtrlBitrate[INFO_LEN]; +extern wchar_t screenShareCtrlShareCursor[INFO_LEN]; +extern wchar_t screenShareCtrlUpdateCaptureParam[INFO_LEN]; +extern wchar_t screenShareCtrlWindowFocus[INFO_LEN]; +extern wchar_t screenShareCtrlExcludeWindowList[INFO_LEN]; + + + + + + +extern wchar_t screenCtrlRectInfo[INFO_LEN]; +extern wchar_t virtualScreenCtrlRectInfo[INFO_LEN]; +extern wchar_t screenCtrlCustomRectInfo[INFO_LEN]; + +//beauty +extern wchar_t beautyCtrlLighteningContrastLevel[INFO_LEN]; +extern wchar_t beautyCtrlLightening[INFO_LEN]; +extern wchar_t beautyCtrlSmoothness[INFO_LEN]; +extern wchar_t beautyCtrlRedness[INFO_LEN]; +extern wchar_t beautyCtrlEnable[INFO_LEN]; + +//beauty audio +extern wchar_t beautyAudioCtrlSetAudioChange[INFO_LEN]; +extern wchar_t beautyAudioCtrlUnSetAudioChange[INFO_LEN]; +extern wchar_t beautyAudioCtrlChange[INFO_LEN]; +extern wchar_t beautyAudioCtrlPreSet[INFO_LEN]; +extern wchar_t beautyAudioCtrlParam1[INFO_LEN]; +extern wchar_t beautyAudioCtrlParam2[INFO_LEN]; + +//set video profile +extern wchar_t videoProfileCtrlWidth[INFO_LEN]; +extern wchar_t videoProfileCtrlHeight[INFO_LEN]; +extern wchar_t videoProfileCtrlFPS[INFO_LEN]; +extern wchar_t videoProfileCtrlBitrate[INFO_LEN]; +extern wchar_t videoProfileCtrldegradationPreference[INFO_LEN]; +extern wchar_t videoProfileCtrlSetVideoProfile[INFO_LEN]; +extern wchar_t videoProfileCtrlUnSetVideoProfile[INFO_LEN]; + + +//set audio profile +extern wchar_t audioProfileCtrlProfile[INFO_LEN]; +extern wchar_t audioProfileCtrlScenario[INFO_LEN]; +extern wchar_t audioProfileCtrlSetAudioProfile[INFO_LEN]; +extern wchar_t audioProfileCtrlUnSetAudioProfile[INFO_LEN]; + +//audio mixing +extern wchar_t audioMixingCtrlMixingPath[INFO_LEN]; +extern wchar_t audioMixingCtrlRepeatTimes[INFO_LEN]; +extern wchar_t audioMixingCtrlSetAudioMixing[INFO_LEN]; +extern wchar_t audioMixingCtrlUnSetAudioMixing[INFO_LEN]; +extern wchar_t audioMixingCtrlOnlyLocal[INFO_LEN]; +extern wchar_t audioMixingCtrlReplaceMicroPhone[INFO_LEN]; + +//audio effect +extern wchar_t AudioEffectCtrlEffectPath[INFO_LEN]; +extern wchar_t AudioEffectCtrlEffect[INFO_LEN]; +extern wchar_t AudioEffectCtrlLoops[INFO_LEN]; +extern wchar_t AudioEffectCtrlGain[INFO_LEN]; +extern wchar_t AudioEffectCtrlPitch[INFO_LEN]; +extern wchar_t AudioEffectCtrlPan[INFO_LEN]; +extern wchar_t AudioEffectCtrlPublish[INFO_LEN]; +extern wchar_t AudioEffectCtrlAddEffect[INFO_LEN]; +extern wchar_t AudioEffectCtrlRemoveEffect[INFO_LEN]; +extern wchar_t AudioEffectCtrlPreLoad[INFO_LEN]; +extern wchar_t AudioEffectCtrlUnPreload[INFO_LEN]; +extern wchar_t AudioEffectCtrlPauseEffect[INFO_LEN]; +extern wchar_t AudioEffectCtrlPlayEffect[INFO_LEN]; +extern wchar_t AudioEffectCtrlPauseAllEffect[INFO_LEN]; +extern wchar_t AudioEffectCtrlResumeEffect[INFO_LEN]; +extern wchar_t AudioEffectCtrlResumeAllEffect[INFO_LEN]; +extern wchar_t AudioEffectCtrlStopAllEffect[INFO_LEN]; +extern wchar_t AudioEffectCtrlStopEffect[INFO_LEN]; +extern wchar_t AudioEffectCtrlVolume[INFO_LEN]; + + + + + +//custom video capture +extern wchar_t customVideoCaptureCtrlCaptureVideoDevice[INFO_LEN]; +extern wchar_t customVideoCaptureCtrlSetExternlCapture[INFO_LEN]; +extern wchar_t customVideoCaptureCtrlCancelExternlCapture[INFO_LEN]; + + +//original video process +extern wchar_t OriginalVideoCtrlProc[INFO_LEN]; +extern wchar_t OriginalVideoCtrlSetProc[INFO_LEN]; +extern wchar_t OriginalVideoCtrlUnSetProc[INFO_LEN]; + + +//custom audio capture +extern wchar_t customAudioCaptureCtrlCaptureAudioDeivce[INFO_LEN]; +extern wchar_t customAudioCaptureCtrlSetExternlCapture[INFO_LEN]; +extern wchar_t customAudioCaptureCtrlCancelExternlCapture[INFO_LEN]; +extern wchar_t customAudioCaptureCtrlSetAudioRender[INFO_LEN]; +extern wchar_t customAudioCaptureCtrlCancelAudioRender[INFO_LEN]; + + + +//original audio process +extern wchar_t OriginalAudioCtrlProc[INFO_LEN]; +extern wchar_t OriginalAudioCtrlSetProc[INFO_LEN]; +extern wchar_t OriginalAudioCtrlUnSetProc[INFO_LEN]; + +//media player +extern wchar_t mediaPlayerCtrlVideoSource[INFO_LEN]; +extern wchar_t mediaPlayerCtrlOpen[INFO_LEN]; +extern wchar_t mediaPlayerCtrlClose[INFO_LEN]; +extern wchar_t mediaPlayerCtrlPause[INFO_LEN]; +extern wchar_t mediaPlayerCtrlPlay[INFO_LEN]; +extern wchar_t mediaPlayerCtrlAttachPlayer[INFO_LEN]; +extern wchar_t mediaPlayerCtrlDettachPlayer[INFO_LEN]; +extern wchar_t mediaPlayerCtrlPublishVideo[INFO_LEN]; +extern wchar_t mediaPlayerCtrlUnPublishVideo[INFO_LEN]; +extern wchar_t mediaPlayerCtrlPublishAudio[INFO_LEN]; +extern wchar_t mediaPlayerCtrlUnPublishAudio[INFO_LEN]; + + +//multi channel +extern wchar_t MultiChannelCtrlChannelList[INFO_LEN]; + + +//per call test +extern wchar_t PerCallTestCtrlAudioInput[INFO_LEN]; +extern wchar_t PerCallTestCtrlAudioOutput[INFO_LEN]; +extern wchar_t PerCallTestCtrlAudioVol[INFO_LEN]; +extern wchar_t PerCallTestCtrlCamera[INFO_LEN]; +extern wchar_t PerCallTestCtrlStartTest[INFO_LEN]; +extern wchar_t PerCallTestCtrlStopTest[INFO_LEN]; + +//audio volume +extern wchar_t AudioVolumeCtrlCapVol[INFO_LEN]; +extern wchar_t AudioVolumeCtrlCapSigVol[INFO_LEN]; +extern wchar_t AudioVolumeCtrlPlaybackVol[INFO_LEN]; +extern wchar_t AudioVolumeCtrlPlaybackSigVol[INFO_LEN]; + + + +//report in call +extern wchar_t ReportInCallCtrlGopTotal[INFO_LEN]; +extern wchar_t ReportInCallCtrlGopRemoteVideo[INFO_LEN]; +extern wchar_t ReportInCallCtrlGopRemoteAudio[INFO_LEN]; +extern wchar_t ReportInCallCtrlTotalUpDownLink[INFO_LEN]; +extern wchar_t ReportInCallCtrlTotalBytes[INFO_LEN]; +extern wchar_t ReportInCallCtrlTotalBitrate[INFO_LEN]; +extern wchar_t ReportInCallCtrlVideoNetWorkDelay[INFO_LEN]; +extern wchar_t ReportInCallCtrlVideoBytes[INFO_LEN]; +extern wchar_t ReportInCallCtrlVideoBitrate[INFO_LEN]; +extern wchar_t ReportInCallCtrlAudioNetWorkDelay[INFO_LEN]; +extern wchar_t ReportInCallCtrlAudioBytes[INFO_LEN]; +extern wchar_t ReportInCallCtrlAudioBitrate[INFO_LEN]; +extern wchar_t ReportInCallCtrlLocalResoultion[INFO_LEN]; +extern wchar_t ReportInCallCtrlLocalFPS[INFO_LEN]; + + +//area code +extern wchar_t RegionConnCtrlAreaCode[INFO_LEN]; + + +//Cross Channel +extern wchar_t CrossChannelCtrlCrossChannel[INFO_LEN]; +extern wchar_t CrossChannelCtrlToken[INFO_LEN]; +extern wchar_t CrossChannelCtrlUid[INFO_LEN]; +extern wchar_t CrossChannelCrossChannelList[INFO_LEN]; +extern wchar_t CrossChannelAddChannel[INFO_LEN]; +extern wchar_t CrossChannelRemoveChannel[INFO_LEN]; +extern wchar_t CrossChannelStartMediaRelay[INFO_LEN]; +extern wchar_t CrossChannelStopMediaRelay[INFO_LEN]; +extern wchar_t CrossChannelUpdateMediaRelay[INFO_LEN]; + + + +extern void InitKeyInfomation(); diff --git a/windows/APIExample/APIExample/RtcChannelHelperPlugin/AgoraRtcChannelPublishHelper.cpp b/windows/APIExample/APIExample/RtcChannelHelperPlugin/AgoraRtcChannelPublishHelper.cpp new file mode 100644 index 000000000..a8ae838e4 --- /dev/null +++ b/windows/APIExample/APIExample/RtcChannelHelperPlugin/AgoraRtcChannelPublishHelper.cpp @@ -0,0 +1,180 @@ +#include "AgoraRtcChannelPublishHelper.h" +#include "utils/ExtendAudioFrameObserver.h" +#include +#include +#include +class MediaPlayerFrameObserver : + public agora::media::base::IAudioFrameObserver, + public agora::media::base::IVideoFrameObserver +{ +public: + agora::rtc::IRtcEngine *rtc_engine; + AgoraRtcChannelPublishHelper *helper; + std::atomic is_push_videoFrame_{false}; + std::atomic is_push_audioFrame_{ true }; + //timeStamp + static UINT64 WINAPI GetHighAccuracyTickCount() + { + LARGE_INTEGER liPC; + LARGE_INTEGER liPF; + + ::QueryPerformanceCounter(&liPC); + ::QueryPerformanceFrequency(&liPF); + + return static_cast(liPC.QuadPart * 1000 / liPF.QuadPart); + } + virtual void onFrame(const agora::media::base::AudioPcmFrame* frame) + { + if(!helper || !is_push_audioFrame_ || !helper->extend_audio_frame_observer_)return; + helper->extend_audio_frame_observer_->pushAudioData((void *)(&frame->data_[0]), frame->bytes_per_sample * frame->samples_per_channel_); + }; + virtual void onFrame(const agora::media::base::VideoFrame* frame) + { + //set externalVideoSource + + int size = frame->width * frame->height; + BYTE *tmp = (BYTE *)malloc(size * 3/2); + if (!rtc_engine || !is_push_videoFrame_) + { + free(tmp); + return; + } + memcpy(tmp, frame->yBuffer, size); + memcpy(tmp + size, frame->uBuffer, size >> 2); + memcpy(tmp+ size + frame->width * frame->height/4, frame->vBuffer, size >> 2); + agora::media::ExternalVideoFrame vframe; + vframe.stride = frame->yStride; + vframe.height = frame->height; + vframe.timestamp = static_cast(GetHighAccuracyTickCount()); + vframe.rotation = 0; + vframe.type = agora::media::ExternalVideoFrame::VIDEO_BUFFER_TYPE::VIDEO_BUFFER_RAW_DATA; + vframe.format = agora::media::ExternalVideoFrame::VIDEO_PIXEL_FORMAT::VIDEO_PIXEL_I420; + vframe.cropLeft = 0; + vframe.cropTop = 0; + vframe.cropBottom = 0; + vframe.cropRight = 0; + vframe.buffer = tmp; + agora::util::AutoPtr mediaEngine; + mediaEngine.queryInterface(rtc_engine, agora::AGORA_IID_MEDIA_ENGINE); + + if (mediaEngine) + mediaEngine->pushVideoFrame(&vframe); + + free(tmp); + + } + virtual ~MediaPlayerFrameObserver() + { + + } +}; + +AgoraRtcChannelPublishHelper::AgoraRtcChannelPublishHelper() +{ +} + + +AgoraRtcChannelPublishHelper::~AgoraRtcChannelPublishHelper() +{ + +} +void AgoraRtcChannelPublishHelper::attachPlayerToRtc(agora::rtc::IRtcEngine* engine, agora::rtc::IMediaPlayer* media_player) +{ + rtc_engine_ = engine; + media_player_ = media_player; + media_player_frame_observer_.reset(new MediaPlayerFrameObserver); + extend_audio_frame_observer_.reset(new CMeidaPlayerAudioFrameObserver); + media_player_->registerAudioFrameObserver(media_player_frame_observer_.get()); + media_player_->registerVideoFrameObserver(media_player_frame_observer_.get()); + extend_audio_frame_observer_->setRemoteVolume(0); + media_player_frame_observer_->rtc_engine = engine; + media_player_frame_observer_->helper = this; + rtc_engine_->setLocalVideoMirrorMode(agora::rtc::VIDEO_MIRROR_MODE_DISABLED); + rtc_engine_->setAudioProfile(agora::rtc::AUDIO_PROFILE_MUSIC_HIGH_QUALITY_STEREO, agora::rtc::AUDIO_SCENARIO_CHATROOM_ENTERTAINMENT); + rtc_engine_->setRecordingAudioFrameParameters(48000, 2, agora::rtc::RAW_AUDIO_FRAME_OP_MODE_READ_WRITE, 960); + rtc_engine_->setPlaybackAudioFrameParameters(48000, 2, agora::rtc::RAW_AUDIO_FRAME_OP_MODE_READ_WRITE, 960); + if (rtc_engine_) { + agora::util::AutoPtr mediaEngine; + mediaEngine.queryInterface(rtc_engine_, agora::AGORA_IID_MEDIA_ENGINE); + if (mediaEngine) + { + mediaEngine->setExternalVideoSource(true, false); + } + if (mediaEngine) + { + mediaEngine->registerAudioFrameObserver(extend_audio_frame_observer_.get()); + } + } + + +} +void AgoraRtcChannelPublishHelper::adjustPublishSignalVolume(int volume) +{ + if (extend_audio_frame_observer_) + { + extend_audio_frame_observer_->setRemoteVolume(volume); + } +} +void AgoraRtcChannelPublishHelper::registerAgoraRtcChannelPublishHelperObserver(AgoraRtcChannelPublishHelperObserver* observer) +{ + media_player_observer_ = observer; + media_player_->registerPlayerObserver(media_player_observer_); +} +void AgoraRtcChannelPublishHelper::adjustPlayoutSignalVolume(int volume) +{ + if (extend_audio_frame_observer_) + { + extend_audio_frame_observer_->setPlayoutSignalVolume(volume); + } +} + +void AgoraRtcChannelPublishHelper::publishAudio() +{ + if(extend_audio_frame_observer_) + extend_audio_frame_observer_->setRemoteVolume(100); + +} +void AgoraRtcChannelPublishHelper::unpublishAudio() +{ + if(extend_audio_frame_observer_) + extend_audio_frame_observer_->setRemoteVolume(0); +} +void AgoraRtcChannelPublishHelper::publishVideo() +{ + if (media_player_frame_observer_) + media_player_frame_observer_->is_push_videoFrame_.store(true); +} +void AgoraRtcChannelPublishHelper::unpublishVideo() +{ + if (media_player_frame_observer_) + media_player_frame_observer_->is_push_videoFrame_.store(false); +} +void AgoraRtcChannelPublishHelper::detachPlayerFromRtc() +{ + if (media_player_frame_observer_) + { + + media_player_frame_observer_->is_push_videoFrame_.store(false); + media_player_frame_observer_->is_push_audioFrame_.store(false); + + } + if (media_player_) + { + media_player_->unregisterAudioFrameObserver(media_player_frame_observer_.get()); + media_player_->unregisterVideoFrameObserver(media_player_frame_observer_.get()); + media_player_->unregisterPlayerObserver(media_player_observer_); + media_player_ = nullptr; + } + if (rtc_engine_) { + agora::util::AutoPtr mediaEngine; + mediaEngine.queryInterface(rtc_engine_, agora::AGORA_IID_MEDIA_ENGINE); + if (mediaEngine) + { + mediaEngine->registerAudioFrameObserver(nullptr); + } + rtc_engine_ = nullptr; + } + +} + + diff --git a/windows/APIExample/APIExample/RtcChannelHelperPlugin/AgoraRtcChannelPublishHelper.h b/windows/APIExample/APIExample/RtcChannelHelperPlugin/AgoraRtcChannelPublishHelper.h new file mode 100644 index 000000000..cf08935e3 --- /dev/null +++ b/windows/APIExample/APIExample/RtcChannelHelperPlugin/AgoraRtcChannelPublishHelper.h @@ -0,0 +1,89 @@ +#pragma once +#include +#include +#include +#include +#include +#include +class MediaPlayerFrameObserver; +class CMeidaPlayerAudioFrameObserver; +class AgoraRtcChannelPublishHelperObserver : public agora::rtc::IMediaPlayerObserver +{ + ; + /** + * @brief Triggered when the player state changes + * + * @param state New player state + * @param ec Player error message + */ + virtual void onPlayerStateChanged(agora::media::MEDIA_PLAYER_STATE state, + agora::media::MEDIA_PLAYER_ERROR ec) + { + + } + + /** + * @brief Triggered when the player progress changes, once every 1 second + * + * @param position Current playback progress, in seconds + */ + virtual void onPositionChanged(const int64_t position) + { + + } + /** + * @brief Triggered when the player have some event + * + * @param event + */ + virtual void onPlayerEvent(agora::media::MEDIA_PLAYER_EVENT event) + { + + }; + + /** + * @brief Triggered when metadata is obtained + * + * @param type Metadata type + * @param data data + * @param length data length + */ + virtual void onMetadata(agora::media::MEDIA_PLAYER_METADATA_TYPE type, const uint8_t* data, + uint32_t length) + { + + } +}; +class AgoraRtcChannelPublishHelper +{ +public: + + std::unique_ptr extend_audio_frame_observer_; + static AgoraRtcChannelPublishHelper *AgoraRtcChannelPublishHelper::Get() + { + static AgoraRtcChannelPublishHelper helper; + return &helper; + } + void attachPlayerToRtc(agora::rtc::IRtcEngine *engine, agora::rtc::IMediaPlayer *media_player); + void registerAgoraRtcChannelPublishHelperObserver(AgoraRtcChannelPublishHelperObserver *observer); + // /ֹͣƵƵ + void publishAudio(); + void unpublishAudio(); + // /ֹͣƵƵ + void publishVideo(); + void unpublishVideo(); + // ͵ƵƵ + void adjustPublishSignalVolume(int volume); + // ڱزƵ + void adjustPlayoutSignalVolume(int volume); + // Ͽ MediaPlayer RTC SDK Ĺ + void detachPlayerFromRtc(); + AgoraRtcChannelPublishHelper(); + ~AgoraRtcChannelPublishHelper(); +private: + std::unique_ptr media_player_frame_observer_; + agora::rtc::IRtcEngine *rtc_engine_; + agora::rtc::IMediaPlayer *media_player_; + agora::rtc::IMediaPlayerObserver *media_player_observer_; +}; + diff --git a/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/AudioCircularBuffer.cc b/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/AudioCircularBuffer.cc new file mode 100644 index 000000000..3cb6fa3cb --- /dev/null +++ b/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/AudioCircularBuffer.cc @@ -0,0 +1,16 @@ +/* +* Copyright (c) 2016 The Agora project authors. All Rights Reserved. +* +* Use of this source code is governed by a BSD-style license +* that can be found in the LICENSE file in the root of the source +* tree. An additional intellectual property rights grant can be found +* in the file PATENTS. All contributing project authors may +* be found in the AUTHORS file in the root of the source tree. +*/ + +#include "AudioCircularBuffer.h" +#include + + + + diff --git a/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/AudioCircularBuffer.h b/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/AudioCircularBuffer.h new file mode 100644 index 000000000..2feb60f0a --- /dev/null +++ b/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/AudioCircularBuffer.h @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2016 The Agora project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef WEBRTC_CHAT_ENGINE_FILE_AUDIO_CIRCULAR_BUFFER_H_ +#define WEBRTC_CHAT_ENGINE_FILE_AUDIO_CIRCULAR_BUFFER_H_ + +#include "scoped_ptr.h" +#include +template +class AudioCircularBuffer { + + public: + typedef Ty value; + AudioCircularBuffer(uint32_t initSize, bool newWay) + : pInt16BufferPtr(nullptr), + bNewWayProcessing(newWay) + { + mInt16BufferLength = initSize; + if (bNewWayProcessing) { + pInt16BufferPtr = new value[sizeof(value) * mInt16BufferLength]; + } + else { + if (!pInt16Buffer.get()) { + pInt16Buffer.reset(new value[sizeof(value) * mInt16BufferLength]); + } + } + } + + ~AudioCircularBuffer() + { + if (pInt16BufferPtr) { + delete [] pInt16BufferPtr; + pInt16BufferPtr = nullptr; + } + } + + void Push(value* data, int length) + { + if (bNewWayProcessing) { + // If the internal buffer is not large enough, first enlarge the buffer + if (mAvailSamples + length > mInt16BufferLength) { + int newLength = (std::max)(length + mAvailSamples + 960, 2 * mInt16BufferLength); + value * tmpBuffer = new value[sizeof(value) * newLength]; + if (mReadPtrPosition + mAvailSamples > mInt16BufferLength) { + int firstCopyLength = mInt16BufferLength - mReadPtrPosition; + + memcpy(tmpBuffer, pInt16BufferPtr + mReadPtrPosition, sizeof(value) * firstCopyLength); + memcpy(tmpBuffer + firstCopyLength, pInt16BufferPtr, sizeof(value) * (mAvailSamples - firstCopyLength)); + } + else { + memcpy(tmpBuffer, pInt16BufferPtr + mReadPtrPosition, sizeof(value) * mAvailSamples); + } + delete [] pInt16BufferPtr; + + // Construct the new internal array + mInt16BufferLength = newLength; + pInt16BufferPtr = tmpBuffer; + mReadPtrPosition = 0; + mWritePtrPosition = mAvailSamples; + memcpy(pInt16BufferPtr + mWritePtrPosition, data, sizeof(value) * length); + mWritePtrPosition += length; + } + else { + int availSlots = mInt16BufferLength - mWritePtrPosition; + if (availSlots < length) { + memcpy(pInt16BufferPtr + mWritePtrPosition, data, sizeof(value) * availSlots); + memcpy(pInt16BufferPtr, data + availSlots, sizeof(value) * (length - availSlots)); + } + else { + memcpy(pInt16BufferPtr + mWritePtrPosition, data, sizeof(value)*length); + } + mWritePtrPosition = IntModule(mWritePtrPosition, length, mInt16BufferLength); + } + mAvailSamples += length; + } + else { + // If the internal buffer is not large enough, first enlarge the buffer + if (length + mAvailSamples > mInt16BufferLength) { + value * tmpBuffer = new value[sizeof(value) * mAvailSamples]; + memmove(tmpBuffer, &pInt16Buffer[mReadPtrPosition], sizeof(value)*mAvailSamples); + + mInt16BufferLength = (length + mAvailSamples) * 2; + pInt16Buffer.reset(new value[sizeof(value) * mInt16BufferLength]); + memmove(&pInt16Buffer[0], tmpBuffer, sizeof(value)*mAvailSamples); + + delete[] tmpBuffer; + mReadPtrPosition = 0; + } + else { + memmove(&pInt16Buffer[0], &pInt16Buffer[mReadPtrPosition], sizeof(value)*mAvailSamples); + } + + memmove(&pInt16Buffer[mAvailSamples], data, sizeof(value)*length); + mAvailSamples += length; + mReadPtrPosition = 0; + } + } + + void Pop(value* data, int length) + { + if (bNewWayProcessing) { + int availSlots = mInt16BufferLength - mReadPtrPosition; + if (availSlots < length) { + memcpy(data, pInt16BufferPtr + mReadPtrPosition, sizeof(value) * availSlots); + memcpy(data + availSlots, pInt16BufferPtr, sizeof(value) * (length - availSlots)); + } + else { + memcpy(data, pInt16BufferPtr + mReadPtrPosition, sizeof(value)*length); + } + mReadPtrPosition = IntModule(mReadPtrPosition, length, mInt16BufferLength); + mAvailSamples -= length; + } + else { + memmove(data, &pInt16Buffer[mReadPtrPosition], sizeof(value)*length); + mAvailSamples -= length; + mReadPtrPosition += length; + } + } + + void Discard(int length) + { + if (bNewWayProcessing) { + mReadPtrPosition = IntModule(mReadPtrPosition, length, mInt16BufferLength); + mAvailSamples -= length; + } + else { + mAvailSamples -= length; + mReadPtrPosition += length; + } + } + + void Reset() + { + mAvailSamples = 0; + mReadPtrPosition = 0; + mWritePtrPosition = 0; + } + + bool dataAvailable(uint32_t requireLength) { + return mAvailSamples >= requireLength; + } + static uint32_t IntModule(uint32_t ptrIndex, int frmLength, int bufLength) + { + if (ptrIndex + frmLength >= bufLength) { + return ptrIndex + frmLength - bufLength; + } + else { + return ptrIndex + frmLength; + } + } + uint32_t mAvailSamples = 0; + uint32_t mReadPtrPosition = 0; + uint32_t mWritePtrPosition = 0; + uint32_t mInt16BufferLength; + value* pInt16BufferPtr; + AgoraRTC::scoped_array pInt16Buffer; + + private: + bool bNewWayProcessing; + + }; +//ptrIndex = (ptrIndex + frmLength) % bufLength + + + +#endif // WEBRTC_CHAT_ENGINE_FILE_AUDIO_CIRCULAR_BUFFER_H_ diff --git a/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/ExtendAudioFrameObserver.cpp b/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/ExtendAudioFrameObserver.cpp new file mode 100644 index 000000000..543f4fd9e --- /dev/null +++ b/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/ExtendAudioFrameObserver.cpp @@ -0,0 +1,161 @@ +#include "ExtendAudioFrameObserver.h" +#include + +CMeidaPlayerAudioFrameObserver::CMeidaPlayerAudioFrameObserver():agoraAudioBuf(new AudioCircularBuffer(2048,true)), play_back_audio_circular_buffer_(new AudioCircularBuffer(2048, true)) +{ + +} +CMeidaPlayerAudioFrameObserver::~CMeidaPlayerAudioFrameObserver() +{ + +} +bool CMeidaPlayerAudioFrameObserver::onRecordAudioFrame(AudioFrame& audioFrame){ + + int bytes = audioFrame.samples * audioFrame.channels * audioFrame.bytesPerSample; + int16_t *tmpBuf = (int16_t *)malloc(bytes); + memcpy(tmpBuf, audioFrame.buffer, bytes); + + if (agoraAudioBuf->mAvailSamples < bytes) { + free(tmpBuf); + return false; + } + mtx.lock(); + int ret = agoraAudioBuf->mAvailSamples - bytes; + if (ret < 0){ + memcpy(audioFrame.buffer, tmpBuf, bytes); + free(tmpBuf); + mtx.unlock(); + return false; + } + char *data = (char *)malloc(sizeof(char)*bytes); + //FILE *fp = NULL; + agoraAudioBuf->Pop(data, bytes); + //fp=fopen("C:/Users/zhanxiaochao/Desktop/out.pcm","ab+"); + + // if(fp){ + // fwrite(data,1,mv_size,fp); + //fclose(fp); + // } + int16_t* p16 = (int16_t*)data; + int16_t *audioBuf = (int16_t *)malloc(bytes); + memcpy(audioBuf, tmpBuf, bytes); + for (int i = 0; i < bytes / 2; ++i) { + int tmp = p16[i] * remote_audio_volume_; + audioBuf[i] = audioBuf[i] * audioMixVolume; + tmp += audioBuf[i]; + + if (tmp > 32767) { + audioBuf[i] = 32767; + } + else if (tmp < -32768) { + audioBuf[i] = -32768; + } + else { + audioBuf[i] += tmp; + } + } + memcpy(audioFrame.buffer, audioBuf, bytes); + free(audioBuf); + free(tmpBuf); + free(p16); + mtx.unlock(); + return true; +} +bool CMeidaPlayerAudioFrameObserver::onPlaybackAudioFrame(AudioFrame& audioFrame){ + int bytes = audioFrame.samples * audioFrame.channels * audioFrame.bytesPerSample; + int16_t *tmpBuf = (int16_t *)malloc(bytes); + memcpy(tmpBuf, audioFrame.buffer, bytes); + + if (play_back_audio_circular_buffer_->mAvailSamples < bytes) { + memcpy(audioFrame.buffer, tmpBuf, bytes); + free(tmpBuf); + return true; + } + std::lock_guard _(mtx_); + int ret = play_back_audio_circular_buffer_->mAvailSamples - bytes; + if (ret < 0) { + memcpy(audioFrame.buffer, tmpBuf, bytes); + free(tmpBuf); + return true; + } + char *data = (char *)malloc(sizeof(char)*bytes); + //FILE *fp = NULL; + play_back_audio_circular_buffer_->Pop(data, bytes); + //fp=fopen("C:/Users/zhanxiaochao/Desktop/out.pcm","ab+"); + + // if(fp){ + // fwrite(data,1,mv_size,fp); + //fclose(fp); + // } + int16_t* p16 = (int16_t*)data; + int16_t *audioBuf = (int16_t *)malloc(bytes); + memcpy(audioBuf, tmpBuf, bytes); + for (int i = 0; i < bytes / 2; ++i) { + int tmp = p16[i] * playout_volume_; + audioBuf[i] = audioBuf[i] * 1; + tmp += audioBuf[i]; + + if (tmp > 32767) { + audioBuf[i] = 32767; + } + else if (tmp < -32768) { + audioBuf[i] = -32768; + } + else { + audioBuf[i] = tmp; + } + } + memcpy(audioFrame.buffer, audioBuf, bytes); + free(audioBuf); + free(tmpBuf); + free(p16); + return true; +} +bool CMeidaPlayerAudioFrameObserver::onPlaybackAudioFrameBeforeMixing(unsigned int uid, AudioFrame& audioFrame){ + + return true; +} +bool CMeidaPlayerAudioFrameObserver::onMixedAudioFrame(AudioFrame& audioFrame){ + return true; +} +void CMeidaPlayerAudioFrameObserver::pushAudioData(void *data,int len){ + + mtx.lock(); + agoraAudioBuf->Push((char *)data, len); + mtx.unlock(); + std::lock_guard _(mtx_); + play_back_audio_circular_buffer_->Push((char *)data, len); + +} +void CMeidaPlayerAudioFrameObserver::reset(){ + agoraAudioBuf.reset(new AudioCircularBuffer(2048, true)); + play_back_audio_circular_buffer_.reset(new AudioCircularBuffer(2048, true)); +} +void CMeidaPlayerAudioFrameObserver::setAudioMixing(bool isAudioMix){ + if(isAudioMix){ + audioMixVolume = 0; + }else{ + audioMixVolume = 1; + } +} +void CMeidaPlayerAudioFrameObserver::setRemoteVolume(int volume) { + mtx.lock(); + if (volume < 0) + { + mtx.unlock(); + return; + } + remote_audio_volume_.store(volume/100.0f); + mtx.unlock(); +} +void CMeidaPlayerAudioFrameObserver::setPlayoutSignalVolume(int volume) +{ + mtx.lock(); + if (volume < 0) + { + mtx.unlock(); + return; + } + playout_volume_ = volume / 100.0f; + mtx.unlock(); +} diff --git a/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/ExtendAudioFrameObserver.h b/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/ExtendAudioFrameObserver.h new file mode 100644 index 000000000..ff488a20e --- /dev/null +++ b/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/ExtendAudioFrameObserver.h @@ -0,0 +1,39 @@ +#pragma once +#include +#include +#include +#include +#include "AudioCircularBuffer.h" +#include "scoped_ptr.h" +#include +using namespace AgoraRTC; +using namespace std; +class CMeidaPlayerAudioFrameObserver:public agora::media::IAudioFrameObserver +{ +private: + std::mutex mtx; + std::mutex mtx_; + std::list play_back_audio_cache_; + float audioMixVolume = 1.0f; + std::atomic remote_audio_volume_{ 1.0f }; + std::atomic playout_volume_{ 1.0f }; + virtual bool onRecordAudioFrame(AudioFrame& audioFrame); + virtual bool onPlaybackAudioFrame(AudioFrame& audioFrame); + virtual bool onMixedAudioFrame(AudioFrame& audioFrame); + virtual bool onPlaybackAudioFrameBeforeMixing(unsigned int uid, AudioFrame& audioFrame); + scoped_ptr> agoraAudioBuf; + scoped_ptr> play_back_audio_circular_buffer_; + +public: + CMeidaPlayerAudioFrameObserver(); + ~CMeidaPlayerAudioFrameObserver(); + int sample_rate; + int re_sample_rate; + int re_channels; + void pushAudioData(void *data, int len); + void reset(); + void setAudioMixing(bool isAudioMix); + void setRemoteVolume(int volume); + void setPlayoutSignalVolume(int volume); +}; + diff --git a/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/constructor_magic.h b/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/constructor_magic.h new file mode 100644 index 000000000..b2aabc574 --- /dev/null +++ b/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/constructor_magic.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2011 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +/* + * WebRtc + * Copy from third_party/libjingle/source/talk/base/constructormagic.h + */ + +#ifndef WEBRTC_SYSTEM_WRAPPERS_INTERFACE_CONSTRUCTOR_MAGIC_H_ +#define WEBRTC_SYSTEM_WRAPPERS_INTERFACE_CONSTRUCTOR_MAGIC_H_ + +#ifndef DISALLOW_ASSIGN +#define DISALLOW_ASSIGN(TypeName) \ + void operator=(const TypeName&) +#endif + +#ifndef DISALLOW_COPY_AND_ASSIGN +// A macro to disallow the evil copy constructor and operator= functions +// This should be used in the private: declarations for a class +#define DISALLOW_COPY_AND_ASSIGN(TypeName) \ + TypeName(const TypeName&); \ + DISALLOW_ASSIGN(TypeName) +#endif + +#ifndef DISALLOW_EVIL_CONSTRUCTORS +// Alternative, less-accurate legacy name. +#define DISALLOW_EVIL_CONSTRUCTORS(TypeName) \ + DISALLOW_COPY_AND_ASSIGN(TypeName) +#endif + +#ifndef DISALLOW_IMPLICIT_CONSTRUCTORS +// A macro to disallow all the implicit constructors, namely the +// default constructor, copy constructor and operator= functions. +// +// This should be used in the private: declarations for a class +// that wants to prevent anyone from instantiating it. This is +// especially useful for classes containing only static methods. +#define DISALLOW_IMPLICIT_CONSTRUCTORS(TypeName) \ + TypeName(); \ + DISALLOW_EVIL_CONSTRUCTORS(TypeName) +#endif + +#endif // WEBRTC_SYSTEM_WRAPPERS_INTERFACE_CONSTRUCTOR_MAGIC_H_ diff --git a/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/scoped_ptr.h b/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/scoped_ptr.h new file mode 100644 index 000000000..0eb86efcf --- /dev/null +++ b/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/scoped_ptr.h @@ -0,0 +1,719 @@ +// (C) Copyright Greg Colvin and Beman Dawes 1998, 1999. +// Copyright (c) 2001, 2002 Peter Dimov +// +// Permission to copy, use, modify, sell and distribute this software +// is granted provided this copyright notice appears in all copies. +// This software is provided "as is" without express or implied +// warranty, and with no claim as to its suitability for any purpose. +// +// See http://www.boost.org/libs/smart_ptr/scoped_ptr.htm for documentation. +// + +// scoped_ptr mimics a built-in pointer except that it guarantees deletion +// of the object pointed to, either on destruction of the scoped_ptr or via +// an explicit reset(). scoped_ptr is a simple solution for simple needs; +// use shared_ptr or std::auto_ptr if your needs are more complex. + +// scoped_ptr_malloc added in by Google. When one of +// these goes out of scope, instead of doing a delete or delete[], it +// calls free(). scoped_ptr_malloc is likely to see much more +// use than any other specializations. + +// release() added in by Google. Use this to conditionally +// transfer ownership of a heap-allocated object to the caller, usually on +// method success. +#ifndef WEBRTC_SYSTEM_WRAPPERS_INTERFACE_SCOPED_PTR_H_ +#define WEBRTC_SYSTEM_WRAPPERS_INTERFACE_SCOPED_PTR_H_ + +#include // for assert +#include // for ptrdiff_t +#include // for free() decl +#include "constructor_magic.h" +#include "template_util.h" +#include "typedefs.h" +#include // for std::swap + +#ifdef _WIN32 +namespace std { using ::ptrdiff_t; }; +#endif // _WIN32 + +namespace AgoraRTC { + +// Function object which deletes its parameter, which must be a pointer. +// If C is an array type, invokes 'delete[]' on the parameter; otherwise, +// invokes 'delete'. The default deleter for scoped_ptr. +template +struct DefaultDeleter { + DefaultDeleter() {} + template DefaultDeleter(const DefaultDeleter& other) { + // IMPLEMENTATION NOTE: C++11 20.7.1.1.2p2 only provides this constructor + // if U* is implicitly convertible to T* and U is not an array type. + // + // Correct implementation should use SFINAE to disable this + // constructor. However, since there are no other 1-argument constructors, + // using a static_assert based on is_convertible<> and requiring + // complete types is simpler and will cause compile failures for equivalent + // misuses. + // + // Note, the is_convertible check also ensures that U is not an + // array. T is guaranteed to be a non-array, so any U* where U is an array + // cannot convert to T*. + enum { T_must_be_complete = sizeof(T) }; + enum { U_must_be_complete = sizeof(U) }; + static_assert(is_convertible::value, + "U* must implicitly convert to T*"); + } + inline void operator()(T* ptr) const { + enum { type_must_be_complete = sizeof(T) }; + delete ptr; + } +}; + +// Specialization of DefaultDeleter for array types. +template +struct DefaultDeleter { + inline void operator()(T* ptr) const { + enum { type_must_be_complete = sizeof(T) }; + delete[] ptr; + } + +private: + // Disable this operator for any U != T because it is undefined to execute + // an array delete when the static type of the array mismatches the dynamic + // type. + // + // References: + // C++98 [expr.delete]p3 + // http://cplusplus.github.com/LWG/lwg-defects.html#938 + template void operator()(U* array) const; +}; + +// Function object which invokes 'free' on its parameter, which must be +// a pointer. Can be used to store malloc-allocated pointers in scoped_ptr: +// +// scoped_ptr foo_ptr( +// static_cast(malloc(sizeof(int)))); +struct FreeDeleter { + inline void operator()(void* ptr) const { + free(ptr); + } +}; + +namespace internal { + + template + struct ShouldAbortOnSelfReset { + template + static internal::NoType Test(const typename U::AllowSelfReset*); + + template + static internal::YesType Test(...); + + static const bool value = + sizeof(Test(0)) == sizeof(internal::YesType); + }; + + // Minimal implementation of the core logic of scoped_ptr, suitable for + // reuse in both scoped_ptr and its specializations. + template + class scoped_ptr_impl { + public: + explicit scoped_ptr_impl(T* p) : data_(p) {} + + // Initializer for deleters that have data parameters. + scoped_ptr_impl(T* p, const D& d) : data_(p, d) {} + + // Templated constructor that destructively takes the value from another + // scoped_ptr_impl. + template + scoped_ptr_impl(scoped_ptr_impl* other) + : data_(other->release(), other->get_deleter()) { + // We do not support move-only deleters. We could modify our move + // emulation to have rtc::subtle::move() and rtc::subtle::forward() + // functions that are imperfect emulations of their C++11 equivalents, + // but until there's a requirement, just assume deleters are copyable. + } + + template + void TakeState(scoped_ptr_impl* other) { + // See comment in templated constructor above regarding lack of support + // for move-only deleters. + reset(other->release()); + get_deleter() = other->get_deleter(); + } + + ~scoped_ptr_impl() { + if (data_.ptr != NULL) { + // Not using get_deleter() saves one function call in non-optimized + // builds. + static_cast(data_)(data_.ptr); + } + } + + void reset(T* p) { + // This is a self-reset, which is no longer allowed for default deleters: + // https://crbug.com/162971 + assert(!ShouldAbortOnSelfReset::value || p == NULL || p != data_.ptr); + + // Note that running data_.ptr = p can lead to undefined behavior if + // get_deleter()(get()) deletes this. In order to prevent this, reset() + // should update the stored pointer before deleting its old value. + // + // However, changing reset() to use that behavior may cause current code to + // break in unexpected ways. If the destruction of the owned object + // dereferences the scoped_ptr when it is destroyed by a call to reset(), + // then it will incorrectly dispatch calls to |p| rather than the original + // value of |data_.ptr|. + // + // During the transition period, set the stored pointer to NULL while + // deleting the object. Eventually, this safety check will be removed to + // prevent the scenario initially described from occurring and + // http://crbug.com/176091 can be closed. + T* old = data_.ptr; + data_.ptr = NULL; + if (old != NULL) + static_cast(data_)(old); + data_.ptr = p; + } + + T* get() const { return data_.ptr; } + + D& get_deleter() { return data_; } + const D& get_deleter() const { return data_; } + + void swap(scoped_ptr_impl& p2) { + // Standard swap idiom: 'using std::swap' ensures that std::swap is + // present in the overload set, but we call swap unqualified so that + // any more-specific overloads can be used, if available. + using std::swap; + swap(static_cast(data_), static_cast(p2.data_)); + swap(data_.ptr, p2.data_.ptr); + } + + T* release() { + T* old_ptr = data_.ptr; + data_.ptr = NULL; + return old_ptr; + } + + T** accept() { + reset(NULL); + return &(data_.ptr); + } + + T** use() { + return &(data_.ptr); + } + + private: + // Needed to allow type-converting constructor. + template friend class scoped_ptr_impl; + + // Use the empty base class optimization to allow us to have a D + // member, while avoiding any space overhead for it when D is an + // empty class. See e.g. http://www.cantrip.org/emptyopt.html for a good + // discussion of this technique. + struct Data : public D { + explicit Data(T* ptr_in) : ptr(ptr_in) {} + Data(T* ptr_in, const D& other) : D(other), ptr(ptr_in) {} + T* ptr; + }; + + Data data_; + + DISALLOW_COPY_AND_ASSIGN(scoped_ptr_impl); + }; + +} // namespace internal + +template +class scoped_ptr { + private: + + T* ptr; + + scoped_ptr(scoped_ptr const &); + scoped_ptr & operator=(scoped_ptr const &); + + public: + + typedef T element_type; + + explicit scoped_ptr(T* p = NULL): ptr(p) {} + scoped_ptr(scoped_ptr &&rhs) { + ptr = rhs.ptr; + rhs.ptr = NULL; + } + + scoped_ptr& operator=(scoped_ptr &&rhs) { + if (this != &rhs) { + ptr = rhs.ptr; + rhs.ptr = NULL; + } + + return *this; + } + + ~scoped_ptr() { + typedef char type_must_be_complete[sizeof(T)]; + delete ptr; + } + + void reset(T* p = NULL) { + typedef char type_must_be_complete[sizeof(T)]; + + if (ptr != p) { + T* obj = ptr; + ptr = p; + // Delete last, in case obj destructor indirectly results in ~scoped_ptr + delete obj; + } + } + + T& operator*() const { + assert(ptr != NULL); + return *ptr; + } + + T* operator->() const { + assert(ptr != NULL); + return ptr; + } + + T* get() const { + return ptr; + } + + void swap(scoped_ptr & b) { + T* tmp = b.ptr; + b.ptr = ptr; + ptr = tmp; + } + + T* release() { + T* tmp = ptr; + ptr = NULL; + return tmp; + } + + T** accept() { + if (ptr) { + delete ptr; + ptr = NULL; + } + return &ptr; + } + + T** use() { + return &ptr; + } +}; + +template inline +void swap(scoped_ptr& a, scoped_ptr& b) { + a.swap(b); +} + + + + +// scoped_array extends scoped_ptr to arrays. Deletion of the array pointed to +// is guaranteed, either on destruction of the scoped_array or via an explicit +// reset(). Use shared_array or std::vector if your needs are more complex. + +template +class scoped_array { + private: + + T* ptr; + + scoped_array(scoped_array const &); + scoped_array & operator=(scoped_array const &); + + public: + + typedef T element_type; + + explicit scoped_array(T* p = NULL) : ptr(p) {} + + ~scoped_array() { + typedef char type_must_be_complete[sizeof(T)]; + delete[] ptr; + } + + void reset(T* p = NULL) { + typedef char type_must_be_complete[sizeof(T)]; + + if (ptr != p) { + T* arr = ptr; + ptr = p; + // Delete last, in case arr destructor indirectly results in ~scoped_array + delete [] arr; + } + } + + T& operator[](ptrdiff_t i) const { + assert(ptr != NULL); + assert(i >= 0); + return ptr[i]; + } + + T* get() const { + return ptr; + } + + void swap(scoped_array & b) { + T* tmp = b.ptr; + b.ptr = ptr; + ptr = tmp; + } + + T* release() { + T* tmp = ptr; + ptr = NULL; + return tmp; + } + + T** accept() { + if (ptr) { + delete [] ptr; + ptr = NULL; + } + return &ptr; + } +}; + +template inline +void swap(scoped_array& a, scoped_array& b) { + a.swap(b); +} + +// scoped_ptr_malloc<> is similar to scoped_ptr<>, but it accepts a +// second template argument, the function used to free the object. + +template class scoped_ptr_malloc { + private: + + T* ptr; + + scoped_ptr_malloc(scoped_ptr_malloc const &); + scoped_ptr_malloc & operator=(scoped_ptr_malloc const &); + + public: + + typedef T element_type; + + explicit scoped_ptr_malloc(T* p = 0): ptr(p) {} + + ~scoped_ptr_malloc() { + FF(static_cast(ptr)); + } + + void reset(T* p = 0) { + if (ptr != p) { + FF(static_cast(ptr)); + ptr = p; + } + } + + T& operator*() const { + assert(ptr != 0); + return *ptr; + } + + T* operator->() const { + assert(ptr != 0); + return ptr; + } + + T* get() const { + return ptr; + } + + void swap(scoped_ptr_malloc & b) { + T* tmp = b.ptr; + b.ptr = ptr; + ptr = tmp; + } + + T* release() { + T* tmp = ptr; + ptr = 0; + return tmp; + } + + T** accept() { + if (ptr) { + FF(static_cast(ptr)); + ptr = 0; + } + return &ptr; + } +}; + +template inline +void swap(scoped_ptr_malloc& a, scoped_ptr_malloc& b) { + a.swap(b); +} + +} // namespace AgoraRTC + +namespace AgoraAPM { + template > + class scoped_ptr { + + // TODO(ajm): If we ever import RefCountedBase, this check needs to be + // enabled. + //static_assert(rtc::internal::IsNotRefCounted::value, + // "T is refcounted type and needs scoped refptr"); + + public: + // The element and deleter types. + typedef T element_type; + typedef D deleter_type; + + // Constructor. Takes ownership of p. + explicit scoped_ptr(element_type* p=NULL) : impl_(p) {} + + // Constructor. Allows initialization of a stateful deleter. + scoped_ptr(element_type* p, const D& d) : impl_(p, d) {} + + // Constructor. Allows construction from a scoped_ptr rvalue for a + // convertible type and deleter. + // + // IMPLEMENTATION NOTE: C++11 unique_ptr<> keeps this constructor distinct + // from the normal move constructor. By C++11 20.7.1.2.1.21, this constructor + // has different post-conditions if D is a reference type. Since this + // implementation does not support deleters with reference type, + // we do not need a separate move constructor allowing us to avoid one + // use of SFINAE. You only need to care about this if you modify the + // implementation of scoped_ptr. +// template +// scoped_ptr(scoped_ptr&& other) +// : impl_(&other.impl_) { +// // static_assert(!AgoraRTC::is_array::value, "U cannot be an array"); +// } +// +// // operator=. Allows assignment from a scoped_ptr rvalue for a convertible +// // type and deleter. +// // +// // IMPLEMENTATION NOTE: C++11 unique_ptr<> keeps this operator= distinct from +// // the normal move assignment operator. By C++11 20.7.1.2.3.4, this templated +// // form has different requirements on for move-only Deleters. Since this +// // implementation does not support move-only Deleters, we do not need a +// // separate move assignment operator allowing us to avoid one use of SFINAE. +// // You only need to care about this if you modify the implementation of +// // scoped_ptr. +// template +// scoped_ptr& operator=(scoped_ptr&& rhs) { +// // static_assert(!AgoraRTC::is_array::value, "U cannot be an array"); +// impl_.TakeState(&rhs.impl_); +// return *this; +// } + + // Deleted copy constructor and copy assignment, to make the type move-only. + private: + scoped_ptr(const scoped_ptr& other); + scoped_ptr& operator=(const scoped_ptr& other); + public: + // Reset. Deletes the currently owned object, if any. + // Then takes ownership of a new object, if given. + void reset(element_type* p = NULL) { impl_.reset(p); } + + // Accessors to get the owned object. + // operator* and operator-> will assert() if there is no current object. + element_type& operator*() const { + assert(impl_.get() != NULL); + return *impl_.get(); + } + element_type* operator->() const { + assert(impl_.get() != NULL); + return impl_.get(); + } + element_type* get() const { return impl_.get(); } + + // Access to the deleter. + deleter_type& get_deleter() { return impl_.get_deleter(); } + const deleter_type& get_deleter() const { return impl_.get_deleter(); } + + // Allow scoped_ptr to be used in boolean expressions, but not + // implicitly convertible to a real bool (which is dangerous). + // + // Note that this trick is only safe when the == and != operators + // are declared explicitly, as otherwise "scoped_ptr1 == + // scoped_ptr2" will compile but do the wrong thing (i.e., convert + // to Testable and then do the comparison). + private: + typedef AgoraRTC::internal::scoped_ptr_impl + scoped_ptr::*Testable; + + public: + operator Testable() const { + return impl_.get() ? &scoped_ptr::impl_ : NULL; + } + + // Comparison operators. + // These return whether two scoped_ptr refer to the same object, not just to + // two different but equal objects. + bool operator==(const element_type* p) const { return impl_.get() == p; } + bool operator!=(const element_type* p) const { return impl_.get() != p; } + + // Swap two scoped pointers. + void swap(scoped_ptr& p2) { + impl_.swap(p2.impl_); + } + + // Release a pointer. + // The return value is the current pointer held by this object. If this object + // holds a NULL, the return value is NULL. After this operation, this + // object will hold a NULL, and will not own the object any more. + element_type* release() WARN_UNUSED_RESULT { + return impl_.release(); + } + + // Delete the currently held pointer and return a pointer + // to allow overwriting of the current pointer address. + element_type** accept() WARN_UNUSED_RESULT { + return impl_.accept(); + } + + // Return a pointer to the current pointer address. + element_type** use() WARN_UNUSED_RESULT { + return impl_.use(); + } + + private: + // Needed to reach into |impl_| in the constructor. + template friend class scoped_ptr; + AgoraRTC::internal::scoped_ptr_impl impl_; + + // Forbidden for API compatibility with std::unique_ptr. + explicit scoped_ptr(int disallow_construction_from_null); + + // Forbid comparison of scoped_ptr types. If U != T, it totally + // doesn't make sense, and if U == T, it still doesn't make sense + // because you should never have the same object owned by two different + // scoped_ptrs. + template bool operator==(scoped_ptr const& p2) const; + template bool operator!=(scoped_ptr const& p2) const; + }; + + template + class scoped_ptr { + public: + // The element and deleter types. + typedef T element_type; + typedef D deleter_type; + + // Constructor. Stores the given array. Note that the argument's type + // must exactly match T*. In particular: + // - it cannot be a pointer to a type derived from T, because it is + // inherently unsafe in the general case to access an array through a + // pointer whose dynamic type does not match its static type (eg., if + // T and the derived types had different sizes access would be + // incorrectly calculated). Deletion is also always undefined + // (C++98 [expr.delete]p3). If you're doing this, fix your code. + // - it cannot be const-qualified differently from T per unique_ptr spec + // (http://cplusplus.github.com/LWG/lwg-active.html#2118). Users wanting + // to work around this may use implicit_cast(). + // However, because of the first bullet in this comment, users MUST + // NOT use implicit_cast() to upcast the static type of the array. + explicit scoped_ptr(element_type* array=NULL) : impl_(array) {} + + // operator=. Allows assignment from a NULL. Deletes the currently owned + // array, if any. + scoped_ptr& operator=(element_type *t) { + reset(t); + return *this; + } + private: + // Deleted copy constructor and copy assignment, to make the type move-only. + scoped_ptr(const scoped_ptr& other); + scoped_ptr& operator=(const scoped_ptr& other); + public: + // Reset. Deletes the currently owned array, if any. + // Then takes ownership of a new object, if given. + void reset(element_type* array = NULL) { impl_.reset(array); } + + // Accessors to get the owned array. + element_type& operator[](size_t i) const { + assert(impl_.get() != NULL); + return impl_.get()[i]; + } + element_type* get() const { return impl_.get(); } + + // Access to the deleter. + deleter_type& get_deleter() { return impl_.get_deleter(); } + const deleter_type& get_deleter() const { return impl_.get_deleter(); } + + // Allow scoped_ptr to be used in boolean expressions, but not + // implicitly convertible to a real bool (which is dangerous). + private: + typedef AgoraRTC::internal::scoped_ptr_impl + scoped_ptr::*Testable; + + public: + operator Testable() const { + return impl_.get() ? &scoped_ptr::impl_ : NULL; + } + + // Comparison operators. + // These return whether two scoped_ptr refer to the same object, not just to + // two different but equal objects. + bool operator==(element_type* array) const { return impl_.get() == array; } + bool operator!=(element_type* array) const { return impl_.get() != array; } + + // Swap two scoped pointers. + void swap(scoped_ptr& p2) { + impl_.swap(p2.impl_); + } + + // Release a pointer. + // The return value is the current pointer held by this object. If this object + // holds a NULL, the return value is NULL. After this operation, this + // object will hold a NULL, and will not own the object any more. + element_type* release() WARN_UNUSED_RESULT { + return impl_.release(); + } + + // Delete the currently held pointer and return a pointer + // to allow overwriting of the current pointer address. + element_type** accept() WARN_UNUSED_RESULT { + return impl_.accept(); +} + +// Return a pointer to the current pointer address. +element_type** use() WARN_UNUSED_RESULT { + return impl_.use(); +} + +private: + // Force element_type to be a complete type. + enum { type_must_be_complete = sizeof(element_type) }; + + // Actually hold the data. + AgoraRTC::internal::scoped_ptr_impl impl_; + + // Disable initialization from any type other than element_type*, by + // providing a constructor that matches such an initialization, but is + // private and has no definition. This is disabled because it is not safe to + // call delete[] on an array whose static type does not match its dynamic + // type. + template explicit scoped_ptr(U* array); + explicit scoped_ptr(int disallow_construction_from_null); + + // Disable reset() from any type other than element_type*, for the same + // reasons as the constructor above. + template void reset(U* array); + void reset(int disallow_reset_from_null); + + // Forbid comparison of scoped_ptr types. If U != T, it totally + // doesn't make sense, and if U == T, it still doesn't make sense + // because you should never have the same object owned by two different + // scoped_ptrs. + template bool operator==(scoped_ptr const& p2) const; + template bool operator!=(scoped_ptr const& p2) const; +}; +} + +#endif // #ifndef WEBRTC_SYSTEM_WRAPPERS_INTERFACE_SCOPED_PTR_H_ diff --git a/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/template_util.h b/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/template_util.h new file mode 100644 index 000000000..3c347cde5 --- /dev/null +++ b/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/template_util.h @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +// Borrowed from Chromium's src/base/template_util.h. + +#ifndef WEBRTC_BASE_TEMPLATE_UTIL_H_ +#define WEBRTC_BASE_TEMPLATE_UTIL_H_ + +#include // For size_t. + +namespace AgoraRTC { + +// Template definitions from tr1. + +template +struct integral_constant { + static const T value = v; + typedef T value_type; + typedef integral_constant type; +}; + +template const T integral_constant::value; + +typedef integral_constant true_type; +typedef integral_constant false_type; + +template struct is_pointer : false_type {}; +template struct is_pointer : true_type {}; + +template struct is_same : public false_type {}; +template struct is_same : true_type {}; + +template struct is_array : public false_type {}; +template struct is_array : public true_type {}; +template struct is_array : public true_type {}; + +template struct is_non_const_reference : false_type {}; +template struct is_non_const_reference : true_type {}; +template struct is_non_const_reference : false_type {}; + +template struct is_void : false_type {}; +template <> struct is_void : true_type {}; + +namespace internal { + +// Types YesType and NoType are guaranteed such that sizeof(YesType) < +// sizeof(NoType). +typedef char YesType; + +struct NoType { + YesType dummy[2]; +}; + +// This class is an implementation detail for is_convertible, and you +// don't need to know how it works to use is_convertible. For those +// who care: we declare two different functions, one whose argument is +// of type To and one with a variadic argument list. We give them +// return types of different size, so we can use sizeof to trick the +// compiler into telling us which function it would have chosen if we +// had called it with an argument of type From. See Alexandrescu's +// _Modern C++ Design_ for more details on this sort of trick. + +struct ConvertHelper { + template + static YesType Test(To); + + template + static NoType Test(...); + + template + static From& Create(); +}; + +// Used to determine if a type is a struct/union/class. Inspired by Boost's +// is_class type_trait implementation. +struct IsClassHelper { + template + static YesType Test(void(C::*)(void)); + + template + static NoType Test(...); +}; + +} // namespace internal + +// Inherits from true_type if From is convertible to To, false_type otherwise. +// +// Note that if the type is convertible, this will be a true_type REGARDLESS +// of whether or not the conversion would emit a warning. +template +struct is_convertible + : integral_constant( + internal::ConvertHelper::Create())) == + sizeof(internal::YesType)> { +}; + +template +struct is_class + : integral_constant(0)) == + sizeof(internal::YesType)> { +}; + +} // namespace AgoraRTC + +#endif // WEBRTC_BASE_TEMPLATE_UTIL_H_ diff --git a/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/typedefs.h b/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/typedefs.h new file mode 100644 index 000000000..c956349bc --- /dev/null +++ b/windows/APIExample/APIExample/RtcChannelHelperPlugin/utils/typedefs.h @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +// This file contains platform-specific typedefs and defines. +// Much of it is derived from Chromium's build/build_config.h. + +#ifndef WEBRTC_TYPEDEFS_H_ +#define WEBRTC_TYPEDEFS_H_ + +// For access to standard POSIXish features, use WEBRTC_POSIX instead of a +// more specific macro. +#if defined(WEBRTC_MAC) || defined(WEBRTC_LINUX) || \ + defined(WEBRTC_ANDROID) +#define WEBRTC_POSIX +#endif + +// Processor architecture detection. For more info on what's defined, see: +// http://msdn.microsoft.com/en-us/library/b0084kay.aspx +// http://www.agner.org/optimize/calling_conventions.pdf +// or with gcc, run: "echo | gcc -E -dM -" +// TODO(andrew): replace WEBRTC_LITTLE_ENDIAN with WEBRTC_ARCH_LITTLE_ENDIAN. +#if defined(_M_X64) || defined(__x86_64__) +#if !defined(WEBRTC_ARCH_X86_FAMILY) + #define WEBRTC_ARCH_X86_FAMILY +#endif +#define WEBRTC_ARCH_X86_64 +#define WEBRTC_ARCH_64_BITS +#define WEBRTC_ARCH_LITTLE_ENDIAN +#define WEBRTC_LITTLE_ENDIAN +#elif defined(__aarch64__) +#define WEBRTC_ARCH_64_BITS +#define WEBRTC_ARCH_LITTLE_ENDIAN +#define WEBRTC_LITTLE_ENDIAN +#elif defined(_M_IX86) || defined(__i386__) +#if !defined(WEBRTC_ARCH_X86_FAMILY) + #define WEBRTC_ARCH_X86_FAMILY +#endif +#define WEBRTC_ARCH_X86 +#define WEBRTC_ARCH_32_BITS +#define WEBRTC_ARCH_LITTLE_ENDIAN +#define WEBRTC_LITTLE_ENDIAN +#elif defined(__ARMEL__) +// TODO(andrew): We'd prefer to control platform defines here, but this is +// currently provided by the Android makefiles. Commented to avoid duplicate +// definition warnings. +//#define WEBRTC_ARCH_ARM +// TODO(andrew): Chromium uses the following two defines. Should we switch? +//#define WEBRTC_ARCH_ARM_FAMILY +//#define WEBRTC_ARCH_ARMEL +#define WEBRTC_ARCH_32_BITS +#define WEBRTC_ARCH_LITTLE_ENDIAN +#define WEBRTC_LITTLE_ENDIAN +#elif defined(__MIPSEL__) +#define WEBRTC_ARCH_32_BITS +#define WEBRTC_ARCH_LITTLE_ENDIAN +#define WEBRTC_LITTLE_ENDIAN +#else +//#error Please add support for your architecture in typedefs.h +#endif + +#if !defined(WARN_UNUSED_RESULT) +#if defined(__GNUC__) +#define WARN_UNUSED_RESULT __attribute__((warn_unused_result)) +#else +#define WARN_UNUSED_RESULT +#endif +#endif // WARN_UNUSED_RESULT + +#if defined(WEBRTC_IOS) +#if defined(__arm__) || defined(__arm64__) +#define WEBRTC_ARCH_ARM_NEON +#endif +#endif + +#if defined(__SSE2__) || defined(_MSC_VER) +#define WEBRTC_USE_SSE2 +#endif + +#if !defined(_MSC_VER) +#include +#else +// Define C99 equivalent types, since MSVC doesn't provide stdint.h. +typedef signed char int8_t; +typedef signed short int16_t; +typedef signed int int32_t; +typedef __int64 int64_t; +typedef unsigned char uint8_t; +typedef unsigned short uint16_t; +typedef unsigned int uint32_t; +typedef unsigned __int64 uint64_t; +#endif + +// Borrowed from Chromium's base/compiler_specific.h. +// Annotate a virtual method indicating it must be overriding a virtual +// method in the parent class. +// Use like: +// virtual void foo() OVERRIDE; +#if defined(_MSC_VER) +#define OVERRIDE override +#elif defined(__clang__) +// Clang defaults to C++03 and warns about using override. Squelch that. +// Intentionally no push/pop here so all users of OVERRIDE ignore the warning +// too. This is like passing -Wno-c++11-extensions, except that GCC won't die +// (because it won't see this pragma). +#pragma clang diagnostic ignored "-Wc++11-extensions" +#define OVERRIDE override +#else +#define OVERRIDE +#endif + +#define LJB +#define AECMax(a,b) ( a > b ? a:b ) +#define IMPROVED + +// Annotate a function that will not return control flow to the caller. +#if defined(_MSC_VER) +#define NO_RETURN __declspec(noreturn) +#elif defined(__GNUC__) +#define NO_RETURN __attribute__((noreturn)) +#else +#define NO_RETURN +#endif + +#ifndef AGORAVOICE_DLLEXPORT2 +#define AGORAVOICE_DLLEXPORT2 +#endif + +// handle DEBUG variants +#if defined(_DEBUG) +#define DEBUG +#endif + +#if defined(DEBUG) +#define VIDEO_INTERNAL_DEBUG +//#define ENABLE_PROFILING +#undef NDEBUG +#endif + +//#define ENABLE_SIMULATE_ERROR +#define ENABLE_PROFILING +// check profiling allowed or not +int isProfilingAllowed(); + +#endif // WEBRTC_TYPEDEFS_H_ diff --git a/windows/APIExample/APIExample/d3d/D3DRender.cpp b/windows/APIExample/APIExample/d3d/D3DRender.cpp new file mode 100644 index 000000000..21ec31d9c --- /dev/null +++ b/windows/APIExample/APIExample/d3d/D3DRender.cpp @@ -0,0 +1,132 @@ +#include "D3DRender.h" + +D3DRender::D3DRender() +{ + InitializeCriticalSection(&m_critial); + m_pDirect3D9 = NULL; + m_pDirect3DDevice = NULL; + m_pDirect3DSurfaceRender = NULL; +} +D3DRender::~D3DRender() +{ + Close(); + DeleteCriticalSection(&m_critial); +} + +//init render hwnd and set width and height. +int D3DRender::Init(HWND hwnd, unsigned int nWidth, unsigned int nHeight, bool isYuv) { + + HRESULT lRet; + + Close(); + + m_pDirect3D9 = Direct3DCreate9(D3D_SDK_VERSION); + if (m_pDirect3D9 == NULL) + return -1; + + D3DPRESENT_PARAMETERS d3dpp; + ZeroMemory(&d3dpp, sizeof(d3dpp)); + d3dpp.Windowed = TRUE; + d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; + d3dpp.BackBufferFormat = D3DFMT_UNKNOWN; + + GetClientRect(hwnd, &m_rtViewport); + + lRet = m_pDirect3D9->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &m_pDirect3DDevice); + if (FAILED(lRet)) + return -1; + + if (isYuv) { + lRet = m_pDirect3DDevice->CreateOffscreenPlainSurface(nWidth, nHeight, (D3DFORMAT)'21VY', D3DPOOL_DEFAULT, &m_pDirect3DSurfaceRender, NULL); + if (FAILED(lRet)) + return -1; + } + else { + lRet = m_pDirect3DDevice->CreateOffscreenPlainSurface(nWidth, nHeight, D3DFMT_X8R8G8B8, D3DPOOL_DEFAULT, &m_pDirect3DSurfaceRender, NULL); + if (FAILED(lRet)) + return -1; + } + + m_nWidth = nWidth; + m_nHeight = nHeight; + m_bIsYuv = isYuv; + + return 0; +} + +void D3DRender::Close() +{ + EnterCriticalSection(&m_critial); + if (m_pDirect3DSurfaceRender) + { + m_pDirect3DSurfaceRender->Release(); + m_pDirect3DSurfaceRender = NULL; + } + if (m_pDirect3DDevice) + { + m_pDirect3DDevice->Release(); + m_pDirect3DDevice = NULL; + } + if (m_pDirect3D9) + { + m_pDirect3D9->Release(); + m_pDirect3D9 = NULL; + } + LeaveCriticalSection(&m_critial); +} + + +bool D3DRender::Render(char *buffer) { + + if (!m_pDirect3DSurfaceRender || !buffer) + return false; + + HRESULT lRet; + D3DLOCKED_RECT d3d_rect; + lRet = m_pDirect3DSurfaceRender->LockRect(&d3d_rect, NULL, D3DLOCK_DONOTWAIT); + if (FAILED(lRet)) + return false; + + byte *pSrc = (byte *)buffer; + byte * pDest = (BYTE *)d3d_rect.pBits; + int stride = d3d_rect.Pitch; + + if (m_bIsYuv) { + for (int i = 0; i < m_nHeight; i++) { + memcpy(pDest + i * stride, pSrc + i * m_nWidth, m_nWidth); + } + for (int i = 0; i < m_nHeight / 2; i++) { + memcpy(pDest + stride * m_nHeight + i * stride / 2, pSrc + m_nWidth * m_nHeight * 5 / 4 + i * m_nWidth / 2, m_nWidth / 2); + } + for (int i = 0; i < m_nHeight / 2; i++) { + memcpy(pDest + stride * m_nHeight + stride * m_nHeight / 4 + i * stride / 2, pSrc + m_nWidth * m_nHeight + i * m_nWidth / 2, m_nWidth / 2); + } + } + else { + int pixel_w_size = m_nWidth * 4; + for (int i = 0; i < m_nHeight; i++) { + memcpy(pDest, pSrc, pixel_w_size); + pDest += stride; + pSrc += pixel_w_size; + } + } + + lRet = m_pDirect3DSurfaceRender->UnlockRect(); + if (FAILED(lRet)) + return false; + + if (m_pDirect3DDevice == NULL) + return false; + + m_pDirect3DDevice->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0); + m_pDirect3DDevice->BeginScene(); + IDirect3DSurface9 * pBackBuffer = NULL; + + m_pDirect3DDevice->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &pBackBuffer); + m_pDirect3DDevice->StretchRect(m_pDirect3DSurfaceRender, NULL, pBackBuffer, &m_rtViewport, D3DTEXF_LINEAR); + m_pDirect3DDevice->EndScene(); + m_pDirect3DDevice->Present(NULL, NULL, NULL, NULL); + pBackBuffer->Release(); + + return true; +} diff --git a/windows/APIExample/APIExample/d3d/D3DRender.h b/windows/APIExample/APIExample/d3d/D3DRender.h new file mode 100644 index 000000000..e339addb9 --- /dev/null +++ b/windows/APIExample/APIExample/d3d/D3DRender.h @@ -0,0 +1,34 @@ +#pragma once +#include +/** + * D3DRender + * You'll need to call the Init function to pass in an HWND and window size + * that supports YUV data and RGB data.The incoming data can then be called to Render. + * + */ + +class D3DRender { + +public: + D3DRender(); + ~D3DRender(); +public: + //initialize window + //hwnd is render to window.nWidth is buffer width not window width,nHeight is buffer height not window height, + //isYuv to identify yuv + int Init(HWND hwnd, unsigned int nWidth, unsigned int nHeight, bool isYuv); + //release d3d handle + void Close(); + //accept buffer data to render window. + bool Render(char *buffer); + +private: + bool m_bIsYuv; + int m_nWidth; + int m_nHeight; + RECT m_rtViewport; + CRITICAL_SECTION m_critial; + IDirect3D9 *m_pDirect3D9; + IDirect3DDevice9 *m_pDirect3DDevice; + IDirect3DSurface9 *m_pDirect3DSurfaceRender; +}; diff --git a/windows/APIExample/APIExample/dsound/DSoundRender.cpp b/windows/APIExample/APIExample/dsound/DSoundRender.cpp new file mode 100644 index 000000000..8a0b1a2a3 --- /dev/null +++ b/windows/APIExample/APIExample/dsound/DSoundRender.cpp @@ -0,0 +1,118 @@ +#include "dsound/DSoundRender.h" + +BOOL DSoundRender::Init(HWND hWnd, int sample_rate, int channels, int bits_per_sample) { + Close(); + std::lock_guard _(m_mutex); + if (FAILED(DirectSoundCreate8(NULL, &m_pDS, NULL))) + { +#ifdef _DEBUG + OutputDebugString(_T("DirectSoundCreate8 error!\n")); +#endif + return FALSE; + } + if (FAILED(m_pDS->SetCooperativeLevel(hWnd, DSSCL_NORMAL))) + { +#ifdef _DEBUG + OutputDebugString(_T("SetCooperativeLevel error!\n")); +#endif + return FALSE; + } + + m_channels = channels; + m_sample_rate = sample_rate; + m_bits_per_sample = bits_per_sample; + + DSBUFFERDESC dsbd; + memset(&dsbd, 0, sizeof(dsbd)); + dsbd.dwSize = sizeof(dsbd); + dsbd.dwFlags = DSBCAPS_GLOBALFOCUS | DSBCAPS_CTRLPOSITIONNOTIFY | DSBCAPS_GETCURRENTPOSITION2; + dsbd.dwBufferBytes = MAX_AUDIO_BUF * BUFFERNOTIFYSIZE; + dsbd.lpwfxFormat = (WAVEFORMATEX*)new WAVEFORMATEX; + dsbd.lpwfxFormat->wFormatTag = WAVE_FORMAT_PCM; + dsbd.lpwfxFormat->nChannels = channels; + dsbd.lpwfxFormat->nSamplesPerSec = sample_rate; + dsbd.lpwfxFormat->nAvgBytesPerSec = sample_rate * (bits_per_sample / 8)*channels; + dsbd.lpwfxFormat->nBlockAlign = (bits_per_sample / 8)*channels; + dsbd.lpwfxFormat->wBitsPerSample = bits_per_sample; + dsbd.lpwfxFormat->cbSize = 0; + + if (FAILED(m_pDS->CreateSoundBuffer(&dsbd, &m_pDSBuffer, NULL))) { +#ifdef _DEBUG + OutputDebugString(_T("SetCooperativeLevel error!\n")); +#endif + return FALSE; + } + if (FAILED(m_pDSBuffer->QueryInterface(IID_IDirectSoundBuffer8, (LPVOID*)&m_pDSBuffer8))) { +#ifdef _DEBUG + OutputDebugString(_T("SetCooperativeLevel error!\n")); +#endif + return FALSE; + } + if (FAILED(m_pDSBuffer8->QueryInterface(IID_IDirectSoundNotify, (LPVOID*)&m_pDSNotify))) { +#ifdef _DEBUG + OutputDebugString(_T("SetCooperativeLevel error!\n")); +#endif + return FALSE; + } + for (int i = 0; i < MAX_AUDIO_BUF; i++) { + m_pDSPosNotify[i].dwOffset = i * BUFFERNOTIFYSIZE; + m_event[i] = ::CreateEvent(NULL, FALSE, FALSE, NULL); + m_pDSPosNotify[i].hEventNotify = m_event[i]; + } + m_pDSNotify->SetNotificationPositions(MAX_AUDIO_BUF, m_pDSPosNotify); + m_pDSNotify->Release(); + m_pDSBuffer8->SetCurrentPosition(0); + m_pDSBuffer8->Play(0, 0, DSBPLAY_LOOPING); + return TRUE; +} + +void DSoundRender::Render(BYTE * buffer, int buffer_len) +{ + LPVOID buf = NULL; + if ((res >= WAIT_OBJECT_0) && (res <= WAIT_OBJECT_0 + 3)) + { + std::lock_guard _(m_mutex); + m_pDSBuffer8->Lock(offset, buffer_len, &buf, (DWORD*)&buffer_len, NULL, NULL, 0); + memcpy(buf, buffer, buffer_len); + offset += BUFFERNOTIFYSIZE; + offset %= (BUFFERNOTIFYSIZE * MAX_AUDIO_BUF); +#ifdef _DEBUG + TCHAR buffer[1024]; +#ifdef _UNICODE + swprintf(buffer, _T("offset:%d ,data_len:%d\n"), offset, buffer_len); +#else + sprintf(buffer, _T("offset:%d ,data_len:%d\n"), offset, buffer_len); +#endif // _UNICODE + OutputDebugString(buffer); +#endif + m_pDSBuffer8->Unlock(buf, buffer_len, NULL, 0); + } + res = WaitForMultipleObjects(MAX_AUDIO_BUF, m_event, FALSE, INFINITE); +} + +void DSoundRender::Close() +{ + std::lock_guard _(m_mutex); + if (m_pDSNotify) + { + m_pDSNotify->Release(); + m_pDSNotify = nullptr; + } + if (m_pDSBuffer8) + { + m_pDSBuffer8->Release(); + m_pDSBuffer8 = nullptr; + m_pDSBuffer = nullptr; + } + if (m_pDS) + { + m_pDS->Release(); + m_pDS = nullptr; + } + for (int i = 0; i < MAX_AUDIO_BUF; i++) { + if (m_event[i]) + CloseHandle(m_event[i]); + } +} + + diff --git a/windows/APIExample/APIExample/dsound/DSoundRender.h b/windows/APIExample/APIExample/dsound/DSoundRender.h new file mode 100644 index 000000000..c7cf20a67 --- /dev/null +++ b/windows/APIExample/APIExample/dsound/DSoundRender.h @@ -0,0 +1,43 @@ +#pragma once +#include +#include +#include "tchar.h" +#include + +#define MAX_AUDIO_BUF 4 +#define BUFFERNOTIFYSIZE 192000 + + +class DSoundRender +{ +public: + DSoundRender() + { + for (int i = 0; i < MAX_AUDIO_BUF; i++) + { + m_event[i] = 0; + } + } + ~DSoundRender() { + Close(); + } + BOOL Init(HWND hWnd, int sample_rate, int channels, int bits_per_sample); + void Render(BYTE * buffer, int buffer_len); + void Close(); + +private: + IDirectSound8 *m_pDS = NULL; + IDirectSoundBuffer8 *m_pDSBuffer8 = NULL; + IDirectSoundBuffer *m_pDSBuffer = NULL; + IDirectSoundNotify8 *m_pDSNotify = NULL; + DSBPOSITIONNOTIFY m_pDSPosNotify[MAX_AUDIO_BUF]; + + HANDLE m_event[MAX_AUDIO_BUF]; + DWORD res = WAIT_OBJECT_0; + DWORD offset = 0; + + int m_sample_rate = 44100; + int m_channels = 2; + int m_bits_per_sample = 16; + std::mutex m_mutex; +}; \ No newline at end of file diff --git a/windows/APIExample/APIExample/en.ini b/windows/APIExample/APIExample/en.ini new file mode 100644 index 000000000..a81ef8c92 --- /dev/null +++ b/windows/APIExample/APIExample/en.ini @@ -0,0 +1,235 @@ +[General] +Common.Group.Doc=Document +Common.Document.Website=Document Website +Common.FAQ.Website=FAQ +Common.Register.Website=Register Account +Common.Demo.Website=Github +Common.Basic.Scene=Basic Scene +Common.Advanced.Scene=Advanced Scene + +Common.Ctrl.ChannelName=Channel Name +Common.Ctrl.JoinChannel=JoinChannel +Common.Ctrl.LeaveChannel=LeaveChannel +Common.Ctrl.ClientRole=Client Role + +Agora.ClientRole.Broadcaster=Broadcaster +Agora.ClientRole.Audience=Audience + +Basic.LiveBroadcasting=LiveBroadcasting +Advanced.RtmpStreaming=Rtmp Streaming +Advanced.RtmpInject=Rtmp Inject +Advanced.Metadata=Video SEI + +Advanced.Beauty=Beauty +Advanced.BeautyAudio=Beauty Audio +Advanced.VideoProfile=Video Profile +Advanced.AudioProfile=Audio Profile +Advanced.AudioMixing=Audio Mixing +Advanced.ScreenCap=Screen Share +Advanced.MediaIOVideoCapture=Media IO Video Capture +Advanced.CustomVideoCapture=Custom Video Capture +Advanced.OriginalVideo=Original Video +Advanced.OriginalAudio=Original Audio +Advanced.CustomAudioCapture=Custom Audio Capture +Advanced.MediaEncrypt=Media Encrypt +Advanced.CustomEncrypt=Custom Encrypt +Advanced.MediaPlayer=MediaPlayer +LiveBroadcasting.Ctrl.Persons=Persons +Advanced.AudioEffect=Audio Effect +Advanced.MultiChannel=Multi Channel +Advanced.PerCallTest=PerCallTest +Advanced.AudioVolume=AudioVolume +Advanced.ReportInCall=ReportInCall +Advanced.RegionConn=Region Connection +Advanced.CrossChannel=CrossChannel + +RtmpInject.Ctrl.Url=Inject Url +RtmpInject.Ctrl.Inject=Inject Url +RtmpInject.Ctrl.Remove=Remove Url +Agora.InjectStatus.StartSuccess=The external video stream imported successfully +Agora.InjectStatus.Exist=The external video stream already exists +Agora.InjectStatus.StartUnAuth=The external video stream to be imported is unauthorized +Agora.InjectStatus.StartTimeout=Import external video stream timeout. +Agora.InjectStatus.StartFailed=Import external video stream failed +Agora.InjectStatus.StopSuccess=The external video stream stopped importing successfully +Agora.InjectStatus.NotFound=No external video stream is found +Agora.InjectStatus.StopUnAuth=The external video stream to be stopped importing is unauthorized +Agora.InjectStatus.StopTimeout=Stop importing external video stream timeout +Agora.InjectStatus.StopFailed=Stop importing external video stream failed +Agora.InjectStatus.Broken=The external video stream is corrupted + +RtmpStreaming.Ctrl.PublishUrl=Publish URL +RtmpStreaming.Ctrl.Add=AddStream +RtmpStreaming.Ctrl.Remove=RemoveStream +RtmpStreaming.Ctrl.RemoveAll=RemoveAll +Agora.RtmpStateChange.IDLE=Idle +Agora.RtmpStateChange.Connecting=The SDK is connecting to Agora's streaming server and the RTMP server. +Agora.RtmpStateChange.Running=The RTMP streaming publishes. +Agora.RtmpStateChange.Running.Success=RTMP Publish Success +Agora.RtmpStateChange.Recovering=The RTMP streaming is recovering +Agora.RtmpStateChange.InvalidArg=Invalid argument used. +Agora.RtmpStateChange.Encrypted=RTMP streaming is encrypted and cannot be published. +Agora.RtmpStateChange.ConnTimeout=Timeout for the RTMP streaming. +Agora.RtmpStateChange.InrealErr=An error occurs in Agora's streaming server. +Agora.RtmpStateChange.ServerErr=An error occurs in the RTMP server. +Agora.RtmpStateChange.TooOften=The RTMP streaming publishes too frequently. +Agora.RtmpStateChange.ReachLimit=The host publishes more than 10 URLs. Delete the unnecessary URLs before adding new ones. +Agora.RtmpStateChange.NotAuth=The host manipulates other hosts' URLs. Check your app logic. +Agora.RtmpStateChange.NotFound=Agora's server fails to find the RTMP streaming. +Agora.RtmpStateChange.NotSupported=The format of the RTMP streaming URL is not supported. + +MetaData.Info=maximum 1024 byte +MetaData.Ctrl.SendSEI=Send SEI +MetaData.Ctrl.Send=Send +MetaData.Ctrl.Clear=Clear + + +ScreenShare.Ctrl.ScreenCap=Window HWND +ScreenShare.Ctrl.StartCap=Share Wnd +ScreenShare.Ctrl.EndCap=Stop Share +Share.Ctrl.Screen.RectInfo=Screen +Share.Ctrl.VirtualScreen.RectInfo=All Virtual Screen +Share.Ctrl.Screen.CustomInfo=Custom +ScreenShare.Ctrl.WindowFocus=WindowFocus +ScreenShare.Ctrl.ExcludeWindowList=ExcludeWindowList + +CustomVideoCapture.Ctrl.CaptureVideo =Caputre Video Device +CustomVideoCapture.Ctrl.SetExternlCap=Set Video Caputre +CustomVideoCapture.Ctrl.CancelExternlCap=Cancel Video Capture + +CustomAudioCapture.Ctrl.CaptureAudio =Capture Audio Device +CustomAudioCapture.Ctrl.SetExternlCap=set Audio Caputre +CustomAudioCapture.Ctrl.CancelExternlCap=Cancel Audio Capture +CustomAudioCapture.Ctrl.SetAudioRender = Set Audio Render +CustomAudioCapture.Ctrl.CancelAudioRender = Cancel Audio Render + +Beauty.Ctrl.LighteningContrastLevel= ContrastLevel +Beauty.Ctrl.Lightening = Lightening(0~10) +Beauty.Ctrl.Smoothness = Smoothness(0~10) +Beauty.Ctrl.Redness= Redness(0~10) +Beauty.Ctrl.Enable=Enable + + +AudioProfile.Ctrl.UnSetAudioProfile=Cancel Audio Profile +AudioProfile.Ctrl.SetAudioProfile=Set Audio Profile +AudioProfile.Ctrl.Profile=Profile +AudioProfile.Ctrl.Scenario=Scenario + +BeautyAudio.Ctrl.SetAudioChange=Set Audio Bueauty +BeautyAudio.Ctrl.UnSetAudioChange=Cancel Audio +BeautyAudio.Ctrl.Change=Beauty Type +BeautyAudio.Ctrl.ReverbPreSet=Beauty Preset +BeautyAudio.Ctrl.BeautyAudioCtrlParam1=param1 +BeautyAudio.Ctrl.BeautyAudioCtrlParam2=param2 + +AudioMixing.Ctrl.MixingPath = Mixing Path +AudioMixing.Ctrl.RepeatTimes = Repeat Times +AudioMixing.Ctrl.SetAudioMixing=Set Audio Mixing +AudioMixing.Ctrl.UnSetAudioMixing=Cancel AudioMixing +AudioMixing.Ctrl.OnlyLocal=Only Local play +AudioMixing.Ctrl.ReplaceMicroPhone=Replace Micro Phone + + +AudioEffect.Ctrl.EffectPath=Effect Path +AudioEffect.Ctrl.Effect=Effect +AudioEffect.Ctrl.Loops=Loops +AudioEffect.Ctrl.Gain=Gain +AudioEffect.Ctrl.Pitch=Pitch +AudioEffect.Ctrl.Pan=Pan +AudioEffect.Ctrl.Publish=Publish +AudioEffect.Ctrl.AddEffect=Add Effect +AudioEffect.Ctrl.RemoveEffect=Remove Effect +AudioEffect.Ctrl.PreLoad=PreLoad +AudioEffect.Ctrl.UnPreload=UnPreload +AudioEffect.Ctrl.PauseEffect=Pause Effect +AudioEffect.Ctrl.PlayEffect=Play Effect +AudioEffect.Ctrl.PauseAllEffect=Pause All Effect +AudioEffect.Ctrl.ResumeEffect=Resume Effect +AudioEffect.Ctrl.ResumeAllEffect=Resume All Effect +AudioEffect.Ctrl.StopAllEffect=Stop All Effect +AudioEffect.Ctrl.StopEffect=Stop Effect +AudioEffect.Ctrl.Volume=Volume + +OriginalVideo.Ctrl.Proc = Proc +OriginalVideo.Ctrl.SetProc = SetProc +OriginalVideo.Ctrl.UnSetProc = UnSetProc + + +OriginalVideo.Ctrl.Proc = Original Video Process +OriginalVideo.Ctrl.SetProc = set process +OriginalVideo.Ctrl.UnSetProc = cancel process + + +OriginalAudio.Ctrl.Proc = Original Audio Proc +OriginalAudio.Ctrl.SetProc = Original Audio SetProc +OriginalAudio.Ctrl.UnSetProc = Original Audio UnSetProc + + +CustomEncrypt.Ctrl.Encrypt=CustomEncrypt +CustomEncrypt.Ctrl.SetEncrypt=SetEncrypt +CustomEncrypt.Ctrl.CancelEncrypt=CancelEncrypt + + +MediaEncrypt.Ctrl.Mode=Encrypt Mode +MediaEncrypt.Ctrl.Secret=Secret +MediaEncrypt.Ctrl.SetEncrypt=Set Encrypt + +MeidaPlayer.Ctrl.VideoSource=VideoSource +MeidaPlayer.Ctrl.Open=Open +MeidaPlayer.Ctrl.Close=Close +MeidaPlayer.Ctrl.Pause=Pause +MeidaPlayer.Ctrl.Play=Play +MeidaPlayer.Ctrl.AttachPlayer=AttachPlayer +MeidaPlayer.Ctrl.DettachPlayer=DettachPlayer +MeidaPlayer.Ctrl.PublishVideo=PublishVideo +MeidaPlayer.Ctrl.UnPublishVideo=UnPublishVideo +MeidaPlayer.Ctrl.PublishAudio=PublishAudio +MeidaPlayer.Ctrl.UnPublishAudio=UnPublishAudio + + + +MultiChannel.Ctrl.ChannelList=Channel List + + + +PerCallTest.Ctrl.AudioInput=Audio Input +PerCallTest.Ctrl.AudioOutput=Audio Output +PerCallTest.Ctrl.AudioVol=Audio Vol +PerCallTest.Ctrl.Camera=Camera +PerCallTest.Ctrl.StartTest=Start Test +PerCallTest.Ctrl.StopTest=Stop Test + +AudioVolume.Ctrl.AudioCapVol=AudioCapVol +AudioVolume.Ctrl.AudioCapSigVol=AudioCapSigVol +AudioVolume.Ctrl.AudioPlaybackVol=AudioPlaybackVol +AudioVolume.Ctrl.AudioPlaybackSigVol=AudioPlaybackSigVol + + + +ReportInCall.Ctrl.LocalFPS = Local FPS +ReportInCall.Ctrl.LocaLResoultion=LocaL Resoultion +ReportInCall.Ctrl.AudioBitrate=Bitrate +ReportInCall.Ctrl.AudioBytes=Bytes +ReportInCall.Ctrl.AudioNetWorkDelay=NetWorkDelay +ReportInCall.Ctrl.GopRemoteAudio=RemoteAudio +ReportInCall.Ctrl.GopRemoteVideo=RemoteVideo +ReportInCall.Ctrl.GopTotal=Total +ReportInCall.Ctrl.TotalBitrate=Bitrate +ReportInCall.Ctrl.TotalBytes=Bytes +ReportInCall.Ctrl.TotalUpDownLink=UpLink/DownLink +ReportInCall.Ctrl.VideoNetWorkDelay=NetWorkDelay +ReportInCall.Ctrl.VideoBitrate=Bitrate +ReportInCall.Ctrl.VideoBytes=Bytes + +RegionConn.Ctrl.AreaCode=Area Code + + +CrossChannel.Ctrl.CrossChannel = CrossChannel +CrossChannel.Ctrl.Token = Token +CrossChannel.Ctrl.Uid = Uid +CrossChannel.Ctrl.CrossChannelList = CrossChannelList +CrossChannel.Ctrl.AddChannel = AddChannel +CrossChannel.Ctrl.RemoveChannel = RemoveChannel +CrossChannel.Ctrl.StartMediaRelay = StartMediaRelay +CrossChannel.Ctrl.StopMediaRelay = StopMediaRelay +CrossChannel.Ctrl.UpdateMediaRelay = UpdateMediaRelay diff --git a/windows/APIExample/APIExample/res/APIExample.ico b/windows/APIExample/APIExample/res/APIExample.ico new file mode 100644 index 000000000..d56fbcdfd Binary files /dev/null and b/windows/APIExample/APIExample/res/APIExample.ico differ diff --git a/windows/APIExample/APIExample/res/APIExample.rc2 b/windows/APIExample/APIExample/res/APIExample.rc2 new file mode 100644 index 000000000..f8e7af3a1 Binary files /dev/null and b/windows/APIExample/APIExample/res/APIExample.rc2 differ diff --git a/windows/APIExample/APIExample/res/IDB_NETWORK_QUALITY.bmp b/windows/APIExample/APIExample/res/IDB_NETWORK_QUALITY.bmp new file mode 100644 index 000000000..1e2c83956 Binary files /dev/null and b/windows/APIExample/APIExample/res/IDB_NETWORK_QUALITY.bmp differ diff --git a/windows/APIExample/APIExample/res/ID_TEST_AUDIO.wav b/windows/APIExample/APIExample/res/ID_TEST_AUDIO.wav new file mode 100644 index 000000000..196920cdb Binary files /dev/null and b/windows/APIExample/APIExample/res/ID_TEST_AUDIO.wav differ diff --git a/windows/APIExample/APIExample/res/bitmap1.bmp b/windows/APIExample/APIExample/res/bitmap1.bmp new file mode 100644 index 000000000..6e8e84062 Binary files /dev/null and b/windows/APIExample/APIExample/res/bitmap1.bmp differ diff --git a/windows/APIExample/APIExample/resource.h b/windows/APIExample/APIExample/resource.h new file mode 100644 index 000000000..1b63af893 --- /dev/null +++ b/windows/APIExample/APIExample/resource.h @@ -0,0 +1,283 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ ɵİļ +// APIExample.rc ʹ +// +#define IDM_ABOUTBOX 0x0010 +#define IDD_ABOUTBOX 100 +#define IDS_ABOUTBOX 101 +#define IDD_APIEXAMPLE_DIALOG 102 +#define IDR_MAINFRAME 128 +#define IDD_DIALOG_LIVEBROADCASTING 130 +#define IDD_DIALOG_RTMPINJECT 131 +#define IDD_DIALOG_RTMP_STREAMING 132 +#define IDD_DIALOG_METADATA 133 +#define IDD_DIALOG_SCREEN_SHARE 134 +#define IDD_DIALOG_CUSTOM_CAPTURE_VIDEO 135 +#define IDD_DIALOG_CUSTOM_CAPTURE_AUDIO 136 +#define IDD_DIALOG_BEAUTY 137 +#define IDB_BITMAP_NETWORK_STATE 137 +#define IDD_DIALOG_AUDIO_PROFILE 138 +#define IDR_TEST_WAVE 138 +#define IDD_DIALOG_BEAUTY_AUDIO 139 +#define IDD_DIALOG_AUDIO_MIX 140 +#define IDD_DIALOG_ORIGINAL_VIDEO 141 +#define IDD_DIALOG_ORIGINAL_AUDIO 142 +#define IDD_DIALOG_CUSTOM_ENCRYPT 143 +#define IDD_DIALOG_ORIGINAL_AUDIO_ 144 +#define IDD_DIALOG_MEDIA_PLAYER 145 +#define IDD_DIALOG_VIDEO_PROFILE 146 +#define IDD_DIALOG_MEDIA_ENCRYPT 147 +#define IDD_DIALOG_CUSTOM_CAPTURE_MEDIA_IO_VIDEO 148 +#define IDD_DIALOG_AUDIO_EFFECT 149 +#define IDD_DIALOG_MULTI_CHANNEL 150 +#define IDD_DIALOG_PERCALL_TEST 151 +#define IDD_DIALOG_VOLUME 152 +#define IDD_DIALOG_PEPORT_IN_CALL 153 +#define IDD_DIALOG_REGIONAL_CONNECTION 154 +#define IDD_DIALOG_CROSS_CHANNEL 155 +#define IDC_BUTTON_FAQ 1000 +#define IDC_BUTTON_DOCUMENT2 1001 +#define IDC_BUTTON_DOCUMENT_WEBSITE 1001 +#define IDC_BUTTON_DEMO 1002 +#define IDC_LIST_BASIC 1003 +#define IDC_BUTTON_REGISTER 1004 +#define IDC_LIST_ADVANCED 1005 +#define IDC_STATIC_GROUP_DOC 1006 +#define IDC_STATIC_MAIN 1008 +#define IDC_STATIC_ADVANCED 1009 +#define IDC_STATIC_VIDEO 1010 +#define IDC_LIST_INFO_BROADCASTING 1011 +#define IDC_COMBO_ROLE 1015 +#define IDC_COMBO_ROLE2 1016 +#define IDC_COMBO_PERSONS 1016 +#define IDC_STATIC_ROLE 1017 +#define IDC_STATIC_PERSONS 1018 +#define IDC_STATIC_CHANNELNAME 1019 +#define IDC_EDIT_CHANNELNAME 1020 +#define IDC_BUTTON1 1021 +#define IDC_BUTTON_JOINCHANNEL 1021 +#define IDC_STATIC_INJECT_URL 1022 +#define IDC_STATIC_SENDSEI 1022 +#define IDC_EDIT_LIGHTENING 1022 +#define IDC_EDIT_AUDIO_MIX_PATH 1022 +#define IDC_STATIC_FPS 1022 +#define IDC_BUTTON_SET_AUDIO_PROC 1022 +#define IDC_STATIC_VIDEO_SOURCE 1022 +#define IDC_EDIT_VIDEO_WIDTH 1022 +#define IDC_EDIT_ENCRYPT_KEY 1022 +#define IDC_BUTTON_LEAVE_CHANNEL 1022 +#define IDC_EDIT_INJECT_URL 1023 +#define IDC_EDIT_SEI 1023 +#define IDC_EDIT_BEAUTY_REDNESS 1023 +#define IDC_EDIT_AUDIO_REPEAT_TIMES 1023 +#define IDC_EDIT_FPS 1023 +#define IDC_EDIT_VIDEO_SOURCE 1023 +#define IDC_EDIT_VIDEO_HEIGHT 1023 +#define IDC_BUTTON_ADDSTREAM 1024 +#define IDC_BUTTON_SEND 1024 +#define IDC_EDIT_BEAUTY_SMOOTHNESS 1024 +#define IDC_STATIC_BITRATE 1024 +#define IDC_BUTTON_OPEN 1024 +#define IDC_EDIT_VIDEO_FPS 1024 +#define IDC_EDIT_AUDIO_REPEAT_TIMES2 1024 +#define IDC_EDIT_AUDIO_AGIN 1024 +#define IDC_BUTTON_REMOVE_STREAM 1025 +#define IDC_EDIT_RECV 1025 +#define IDC_EDIT_BITRATE 1025 +#define IDC_BUTTON_STOP 1025 +#define IDC_EDIT_AUDIO_REPEAT_TIMES3 1025 +#define IDC_EDIT_AUDIO_PITCH 1025 +#define IDC_BUTTON_REMOVE_ALLSTREAM 1026 +#define IDC_BUTTON_PLAY 1026 +#define IDC_STATIC_WND_LIST 1026 +#define IDC_BUTTON_ATTACH 1027 +#define IDC_BUTTON_PUBLISH_VIDEO 1028 +#define IDC_BUTTON_PUBLISH_AUDIO 1029 +#define IDC_STATIC_RTMP_URL 1030 +#define IDC_EDIT_RTMP_URL 1031 +#define IDC_STATIC_RTMP_URLS 1032 +#define IDC_COMBO_RTMPURLS 1033 +#define IDC_STATIC_ADVANCE 1034 +#define IDC_STATIC_LIST_INFO 1035 +#define IDC_STATIC_GROUP_LIST 1037 +#define IDC_STATIC_DETAIL 1038 +#define IDC_STATIC_METADATA_INFO 1039 +#define IDC_BUTTON_CLEAR 1040 +#define IDC_STATIC_SCREEN_CAPTURE 1041 +#define IDC_COMBO_SCREEN_CAPTURE 1042 +#define IDC_BUTTON_START_CAPUTRE 1043 +#define IDC_STATIC_CAPTUREDEVICE 1044 +#define IDC_STATIC_BEAUTY_LIGHTENING 1044 +#define IDC_BUTTON_SHARE_DESKTOP 1044 +#define IDC_STATIC_SCREEN_SHARE 1044 +#define IDC_COMBO_CAPTURE_VIDEO_DEVICE 1045 +#define IDC_COMBO_SCREEN_SCREEN 1045 +#define IDC_BUTTON_RENDER_AUDIO 1045 +#define IDC_COMBO_CAPTURE_TYPE 1046 +#define IDC_COMBO_CAPTURE_VIDEO_TYPE 1046 +#define IDC_BUTTON_START_SHARE_SCREEN 1046 +#define IDC_COMBO_CAPTURE_AUDIO_DEVICE 1047 +#define IDC_COMBO_EXLUDE_WINDOW_LIST 1047 +#define IDC_COMBO_CAPTURE_AUDIO_TYPE 1048 +#define IDC_STATIC_BEAUTY_LIGHTENING_CONTRAST_LEVEL 1049 +#define IDC_COMBO_BEAUTE_LIGHTENING_CONTRAST_LEVEL 1050 +#define IDC_STATIC_BEAUTY_REDNESS 1051 +#define IDC_STATIC_BEAUTY_SMOOTHNESS 1052 +#define IDC_CHECK1 1054 +#define IDC_CHECK_BEAUTY_ENABLE 1054 +#define IDC_CHK_ONLY_LOCAL 1054 +#define IDC_CHECK_CURSOR 1054 +#define IDC_CHK_TRANS_CODING 1054 +#define IDC_STATIC_ADUIO_PROFILE 1055 +#define IDC_CHK_REPLACE_MICROPHONE 1055 +#define IDC_STATIC_ADUIO_SCENARIO 1056 +#define IDC_COMBO_AUDIO_PROFILE 1057 +#define IDC_STATIC_CAMERA 1057 +#define IDC_COMBO_AUDIO_SCENARIO 1058 +#define IDC_BUTTON_SET_AUDIO_PROFILE 1059 +#define IDC_STATIC_AUDIO_CHANGER 1060 +#define IDC_COMBO_AUDIO_CHANGER 1061 +#define IDC_BUTTON_SET_AUDIO_CHANGE 1062 +#define IDC_STATIC_AUDIO_REVERB_PRESET 1062 +#define IDC_STATIC_BEAUTY_AUDIO_TYPE 1062 +#define IDC_STATIC_AUDIO_MIX 1063 +#define IDC_STATIC_GENERAL 1063 +#define IDC_COMBO_AUDIO_CHANGER2 1063 +#define IDC_COMBO_AUDIO_PERVERB_PRESET 1063 +#define IDC_BUTTON_SET_AUDIO_MIX 1064 +#define IDC_BUTTON_UPDATEPARAM 1064 +#define IDC_STATIC_AUDIO_REPEAT 1065 +#define IDC_STATIC_ORIGINAL_VIDEO_PROC 1065 +#define IDC_COMBO_SCREEN_REGION 1065 +#define IDC_COMBO_ORIGINAL_VIDEO_PROC 1066 +#define IDC_STATIC_SHARE_DESKTOP 1066 +#define IDC_STATIC_AUDIO_AGIN 1066 +#define IDC_STATIC_AUDIO_VOLUME 1066 +#define IDC_BUTTON_SET_ORIGINAL_PROC 1067 +#define IDC_COMBO_REGION_RECT 1067 +#define IDC_STATIC_AUDIO_VLOUME 1067 +#define IDC_STATIC_REGION_RECT 1068 +#define IDC_STATIC_SCREEN_INFO 1069 +#define IDC_STATIC_SCREEN_INFO2 1070 +#define IDC_STATIC_ORIGINAL_AUDIO 1070 +#define IDC_STATIC_ORIGINAL_AUDIO_PROC 1071 +#define IDC_COMBO_ORIGINAL_AUDIO 1071 +#define IDC_STATIC_CUSTOM_ENCRYPT 1071 +#define IDC_COMBO_ORIGINAL_AUDIO_PROC 1072 +#define IDC_COMBO_CUSTOM_ENCRYPT 1072 +#define IDC_BUTTON_SET_CUSTOM_ENCRYPT 1073 +#define IDC_SLIDER_VIDEO 1075 +#define IDC_STATIC_VIDEO_WIDTH 1076 +#define IDC_STATIC_VIDEO_HEIGHT 1077 +#define IDC_STATIC_VIDEO_FPS 1078 +#define IDC_STATIC_VIDEO_BITRATE 1079 +#define IDC_EDIT_VIDEO_BITRATE 1080 +#define IDC_BUTTON_SET_VIDEO_PROFILE 1081 +#define IDC_STATIC_VIDEO_DEGRADATION_PREFERENCE 1082 +#define IDC_COMBO_DEGRADATION_PREFERENCE 1083 +#define IDC_RADIO_AUDIO_CHANGE 1084 +#define IDC_RADIO_AUDIO_REVERB_PRESET 1085 +#define IDC_BUTTON_SET_BEAUTY_AUDIO 1085 +#define IDC_STATIC_ENCRYPT_MODE 1086 +#define IDC_COMBO_ENCRYPT_MODE 1087 +#define IDC_BUTTON_SET_MEDIA_ENCRYPT 1088 +#define IDC_STATIC_ENCRYPT_KEY 1089 +#define IDC_CHECK_WINDOW_FOCUS 1090 +#define IDC_COMBO_FPS 1091 +#define IDC_STATIC_AUDIO_EFFECT_PATH 1092 +#define IDC_EDIT_AUDIO_EFFECT_PATH 1093 +#define IDC_SPIN1 1094 +#define IDC_SPIN_AGIN 1094 +#define IDC_STATIC_AUDIO_PITCH 1095 +#define IDC_SPIN2 1096 +#define IDC_SPIN_PITCH 1096 +#define IDC_STATIC_AUDIO_PAN 1097 +#define IDC_COMBO_PAN 1098 +#define IDC_CHK_PUBLISH 1099 +#define IDC_BUTTON_ADD_EFFECT 1100 +#define IDC_STATIC_AUDIO_EFFECT 1101 +#define IDC_COMBO2 1102 +#define IDC_BUTTON_REMOVE 1103 +#define IDC_BUTTON_PRELOAD 1104 +#define IDC_BUTTON_PLAY_EFFECT 1105 +#define IDC_BUTTON_PAUSE_EFFECT 1106 +#define IDC_BUTTON_PAUSE_ALL_EFFECT 1107 +#define IDC_BUTTON_UNLOAD_EFFECT 1108 +#define IDC_BUTTON_STOP_EFFECT 1109 +#define IDC_BUTTON_RESUME 1110 +#define IDC_BUTTON_RESUME_EFFECT 1110 +#define IDC_SLIDER_VLOUME 1111 +#define IDC_BUTTON_STOP_ALL_EFFECT2 1112 +#define IDC_SLIDER_VOLUME 1112 +#define IDC_STATIC_CHANNEL_LIST 1113 +#define IDC_SLIDER_CAP_VOLUME 1113 +#define IDC_COMBO_CHANNEL_LIST 1114 +#define IDC_SLIDER_SIGNAL_VOLUME2 1114 +#define IDC_STATIC_ADUIO_INPUT 1115 +#define IDC_SLIDER_PLAYBACK_SIGNAL_VOLUME 1115 +#define IDC_COMBO_AUDIO_INPUT 1116 +#define IDC_SLIDER_PLAYBACK_VOLUME 1116 +#define IDC_STATIC_ADUIO_INPUT_VOL 1117 +#define IDC_COMBO_AUDIO_OUTPUT 1118 +#define IDC_STATIC_ADUIO_OUTPUT_VOL 1119 +#define IDC_SLIDER_INPUT_VOL 1120 +#define IDC_SLIDER_OUTPUT_VOL 1121 +#define IDC_BUTTON_AUDIO_INPUT_TEST 1122 +#define IDC_BUTTON_AUDIO_OUTPUT_TEST 1123 +#define IDC_STATIC_AUDIO_CAP_VOL 1123 +#define IDC_COMBO_VIDEO 1124 +#define IDC_STATIC_AUDIO_SIGNAL_VOL 1124 +#define IDC_BUTTON_CAMERA 1125 +#define IDC_STATIC_PLAYBACK_VOL 1125 +#define IDC_STATIC_PLAYBACK_VOL_SIGNAL 1126 +#define IDC_STATIC_SPEAKER_INFO 1127 +#define IDC_STATIC_TXBYTES_RXBTYES 1130 +#define IDC_STATIC_TXBYTES_RXBYTES_VAL 1131 +#define IDC_STATIC_BITRATE_ALL_VAL 1132 +#define IDC_STATIC_BITRATE_ALL 1133 +#define IDC_STATIC_AUDIO_NETWORK_DELAY 1134 +#define IDC_STATIC_AUDIO_NETWORK_DELAY_VAL 1135 +#define IDC_STATIC_AUDIO_RECIVED_BITRATE 1136 +#define IDC_STATIC_AUDIO_RECVIED_BITRATE_VAL 1137 +#define IDC_STATIC_VIDEO_NETWORK_DELAY 1138 +#define IDC_STATIC_VEDIO_NETWORK_DELAY_VAL 1139 +#define IDC_STATIC_VEDIO_RECIVED_BITRATE 1140 +#define IDC_STATIC_VEDIO_RECVIED_BITRATE_VAL2 1141 +#define IDC_STATIC_LOCAL_VIDEO_WIDTH_HEIGHT 1142 +#define IDC_STATIC_LOCAL_VIDEO_WITH_HEIGHT_VAL 1143 +#define IDC_STATIC_LOCAL_VIDEO_FPS 1144 +#define IDC_STATIC_VIDEO_REMOTE 1145 +#define IDC_STATIC_AUDIO_REMOTE 1146 +#define IDC_STATIC_AREA_CODE 1146 +#define IDC_STATIC_NETWORK_TOTAL 1147 +#define IDC_COMBO_AREA_CODE 1147 +#define IDC_STATIC_LOCAL_VIDEO_FPS_VAL 1148 +#define IDC_STATIC_CROSS_CHANNEL 1148 +#define IDC_EDIT_CROSS_CHANNEL 1149 +#define IDC_EDIT_TOKEN 1150 +#define IDC_STATIC_TOKEN 1151 +#define IDC_USER_ID 1152 +#define IDC_EDIT_USER_ID 1153 +#define IDC_BUTTON_ADD_CROSS_CHANNEL 1154 +#define IDC_CROSS_CHANNEL_LIST 1155 +#define IDC_COMBO_CROSS_CAHNNEL_LIST 1156 +#define IDC_BUTTON_REMOVE_CROSS_CHANNEL2 1157 +#define IDC_BUTTON_START_MEDIA_RELAY 1158 +#define IDC_BUTTON_START_MEDIA_RELAY2 1159 +#define IDC_BUTTON_UPDATE 1159 +#define IDC_EDIT_PARAM1 1160 +#define IDC_EDIT2 1161 +#define IDC_EDIT_PARAM2 1161 +#define IDC_STATIC_PARAM1 1162 +#define IDC_STATIC_PARAM2 1163 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 139 +#define _APS_NEXT_COMMAND_VALUE 32771 +#define _APS_NEXT_CONTROL_VALUE 1164 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/APIExample/APIExample/stdafx.cpp b/windows/APIExample/APIExample/stdafx.cpp new file mode 100644 index 000000000..c969b3683 --- /dev/null +++ b/windows/APIExample/APIExample/stdafx.cpp @@ -0,0 +1,623 @@ + +// stdafx.cpp : source file that includes just the standard includes +// APIExample.pch will be the pre-compiled header +// stdafx.obj will contain the pre-compiled type information + +#include "stdafx.h" +#include "CConfig.h" +wchar_t commonGroupDoc[INFO_LEN] = { 0 }; +wchar_t commonDocumentWebsite[INFO_LEN] = { 0 }; +wchar_t commonFAQWebsite[INFO_LEN] = { 0 }; +wchar_t commonRegisterWebsite[INFO_LEN] = { 0 }; +wchar_t commonDemoWebsite[INFO_LEN] = { 0 }; +wchar_t commonBasicScene[INFO_LEN] = { 0 }; +wchar_t commonAdvanceScene[INFO_LEN] = { 0 }; +//agora enum +wchar_t agoraRoleBroadcaster[INFO_LEN] = { 0 }; +wchar_t agoraRoleAudience[INFO_LEN] = { 0 }; +//common ctrl +wchar_t commonCtrlChannel[INFO_LEN] = { 0 }; +wchar_t commonCtrlJoinChannel[INFO_LEN] = { 0 }; +wchar_t commonCtrlLeaveChannel[INFO_LEN] = { 0 }; +wchar_t commonCtrlClientRole[INFO_LEN] = { 0 }; +//scene list +wchar_t basicLiveBroadcasting[INFO_LEN] = { 0 }; +wchar_t advancedRtmpInject[INFO_LEN] = { 0 }; +wchar_t advancedRtmpStreaming[INFO_LEN] = { 0 }; +wchar_t advancedVideoMetadata[INFO_LEN] = { 0 }; +wchar_t advancedCustomEncrypt[INFO_LEN] = { 0 }; +wchar_t advancedMediaEncrypt[INFO_LEN] = { 0 }; + +wchar_t advancedScreenCap[INFO_LEN] = { 0 }; +wchar_t advancedBeauty[INFO_LEN] = { 0 }; +wchar_t advancedBeautyAudio[INFO_LEN] = { 0 }; +wchar_t advancedVideoProfile[INFO_LEN] = { 0 }; +wchar_t advancedAudioProfile[INFO_LEN] = { 0 }; +wchar_t advancedAudioMixing[INFO_LEN] = { 0 }; +wchar_t advancedCustomVideoCapture[INFO_LEN] = { 0 }; +wchar_t advancedMediaIOCustomVideoCapture[INFO_LEN] = { 0 }; + +wchar_t advancedOriginalVideo[INFO_LEN] = { 0 }; +wchar_t advancedMediaAudioCapture[INFO_LEN] = { 0 }; +wchar_t advancedCustomAudioCapture[INFO_LEN] = { 0 }; +wchar_t advancedOriginalAudio[INFO_LEN] = { 0 }; +wchar_t advancedMediaPlayer[INFO_LEN] = { 0 }; +wchar_t advancedAudioEffect[INFO_LEN] = { 0 }; +wchar_t advancedMultiChannel[INFO_LEN] = { 0 }; +wchar_t advancedPerCallTest[INFO_LEN] = { 0 }; +wchar_t advancedAudioVolume[INFO_LEN] = { 0 }; +wchar_t advancedReportInCall[INFO_LEN] = { 0 }; +wchar_t advancedRegionConn[INFO_LEN] = { 0 }; +wchar_t advancedCrossChannel[INFO_LEN] = { 0 }; +//live broadcasting +wchar_t liveCtrlPersons[INFO_LEN] = { 0 }; + +//rtmp streaming +wchar_t rtmpStreamingCtrlPublishUrl[INFO_LEN] = { 0 }; +wchar_t rtmpStreamingCtrlTransCoding[INFO_LEN] = { 0 }; +wchar_t rtmpStreamingCtrlAdd[INFO_LEN] = { 0 }; +wchar_t rtmpStreamingCtrlRemove[INFO_LEN] = { 0 }; +wchar_t rtmpStreamingCtrlRemoveAll[INFO_LEN] = { 0 }; +wchar_t agoraRtmpStateIdle[INFO_LEN] = { 0 }; +wchar_t agoraRtmpStateConnecting[INFO_LEN] = { 0 }; +wchar_t agoraRtmpStateRunning[INFO_LEN] = { 0 }; +wchar_t agoraRtmpStateRunningSuccess[INFO_LEN] = { 0 }; +wchar_t agoraRtmpStateRecovering[INFO_LEN] = { 0 }; +wchar_t agoraRtmpStateInvalidArg[INFO_LEN] = { 0 }; +wchar_t agoraRtmpStateEncrypted[INFO_LEN] = { 0 }; +wchar_t agoraRtmpStateConnTimeout[INFO_LEN] = { 0 }; +wchar_t agoraRtmpStateInrealErr[INFO_LEN] = { 0 }; +wchar_t agoraRtmpStateServerErr[INFO_LEN] = { 0 }; +wchar_t agoraRtmpStateTooOften[INFO_LEN] = { 0 }; +wchar_t agoraRtmpStateReachLimit[INFO_LEN] = { 0 }; +wchar_t agoraRtmpStateNotAuth[INFO_LEN] = { 0 }; +wchar_t agoraRtmpStateNotFound[INFO_LEN] = { 0 }; +wchar_t agoraRtmpStateNotSupported[INFO_LEN] = { 0 }; + +//rtmp Inject +wchar_t rtmpInjectCtrlUrl[INFO_LEN] = { 0 }; +wchar_t rtmpInjectCtrlInject[INFO_LEN] = { 0 }; +wchar_t rtmpInjectCtrlRemove[INFO_LEN] = { 0 }; + +wchar_t agoraInjectStartSucc[INFO_LEN] = { 0 }; +wchar_t agoraInjectExist[INFO_LEN] = { 0 }; +wchar_t agoraInjectStartUnAuth[INFO_LEN] = { 0 }; +wchar_t agoraInjectStartTimeout[INFO_LEN] = { 0 }; +wchar_t agoraInjectStartFailed[INFO_LEN] = { 0 }; +wchar_t agoraInjectStopSuccess[INFO_LEN] = { 0 }; +wchar_t agoraInjectNotFound[INFO_LEN] = { 0 }; +wchar_t agoraInjectStopUnAuth[INFO_LEN] = { 0 }; +wchar_t agoraInjectStopTimeout[INFO_LEN] = { 0 }; +wchar_t agoraInjectStopFailed[INFO_LEN] = { 0 }; +wchar_t agoraInjectBroken[INFO_LEN] = { 0 }; +//video SEI +wchar_t videoSEIInformation[INFO_LEN] = { 0 }; +wchar_t metadataCtrlSendSEI[INFO_LEN] = { 0 }; +wchar_t metadataCtrlBtnSend[INFO_LEN] = { 0 }; +wchar_t metadataCtrlBtnClear[INFO_LEN] = { 0 }; + +//beauty +wchar_t beautyCtrlLighteningContrastLevel[INFO_LEN] = { 0 }; +wchar_t beautyCtrlLightening[INFO_LEN] = { 0 }; +wchar_t beautyCtrlSmoothness[INFO_LEN] = { 0 }; +wchar_t beautyCtrlRedness[INFO_LEN] = { 0 }; +wchar_t beautyCtrlEnable[INFO_LEN] = { 0 }; + +//beauty audio +wchar_t beautyAudioCtrlSetAudioChange[INFO_LEN] = { 0 }; +wchar_t beautyAudioCtrlUnSetAudioChange[INFO_LEN] = { 0 }; +wchar_t beautyAudioCtrlChange[INFO_LEN] = { 0 }; +wchar_t beautyAudioCtrlPreSet[INFO_LEN] = { 0 }; +wchar_t beautyAudioCtrlParam1[INFO_LEN] = { 0 }; +wchar_t beautyAudioCtrlParam2[INFO_LEN] = { 0 }; + + +//set video profile +wchar_t videoProfileCtrlWidth[INFO_LEN] = { 0 }; +wchar_t videoProfileCtrlHeight[INFO_LEN] = { 0 }; +wchar_t videoProfileCtrlFPS[INFO_LEN] = { 0 }; +wchar_t videoProfileCtrlBitrate[INFO_LEN] = { 0 }; +wchar_t videoProfileCtrldegradationPreference[INFO_LEN] = { 0 }; +wchar_t videoProfileCtrlSetVideoProfile[INFO_LEN] = { 0 }; +wchar_t videoProfileCtrlUnSetVideoProfile[INFO_LEN] = { 0 }; + +//set audio profile +wchar_t audioProfileCtrlProfile[INFO_LEN] = { 0 }; +wchar_t audioProfileCtrlScenario[INFO_LEN] = { 0 }; +wchar_t audioProfileCtrlSetAudioProfile[INFO_LEN] = { 0 }; +wchar_t audioProfileCtrlUnSetAudioProfile[INFO_LEN] = { 0 }; + +//audio mixing +wchar_t audioMixingCtrlMixingPath[INFO_LEN] = { 0 }; +wchar_t audioMixingCtrlRepeatTimes[INFO_LEN] = { 0 }; +wchar_t audioMixingCtrlSetAudioMixing[INFO_LEN] = { 0 }; +wchar_t audioMixingCtrlUnSetAudioMixing[INFO_LEN] = { 0 }; +wchar_t audioMixingCtrlOnlyLocal[INFO_LEN] = { 0 }; +wchar_t audioMixingCtrlReplaceMicroPhone[INFO_LEN] = { 0 }; + +//audio effect +wchar_t AudioEffectCtrlEffectPath[INFO_LEN] = { 0 }; +wchar_t AudioEffectCtrlEffect[INFO_LEN] = { 0 }; +wchar_t AudioEffectCtrlLoops[INFO_LEN] = { 0 }; +wchar_t AudioEffectCtrlGain[INFO_LEN] = { 0 }; +wchar_t AudioEffectCtrlPitch[INFO_LEN] = { 0 }; +wchar_t AudioEffectCtrlPan[INFO_LEN] = { 0 }; +wchar_t AudioEffectCtrlPublish[INFO_LEN] = { 0 }; +wchar_t AudioEffectCtrlAddEffect[INFO_LEN] = { 0 }; +wchar_t AudioEffectCtrlRemoveEffect[INFO_LEN] = { 0 }; +wchar_t AudioEffectCtrlPreLoad[INFO_LEN] = { 0 }; +wchar_t AudioEffectCtrlUnPreload[INFO_LEN] = { 0 }; +wchar_t AudioEffectCtrlPauseEffect[INFO_LEN] = { 0 }; +wchar_t AudioEffectCtrlPlayEffect[INFO_LEN] = { 0 }; +wchar_t AudioEffectCtrlPauseAllEffect[INFO_LEN] = { 0 }; +wchar_t AudioEffectCtrlResumeEffect[INFO_LEN] = { 0 }; +wchar_t AudioEffectCtrlResumeAllEffect[INFO_LEN] = { 0 }; +wchar_t AudioEffectCtrlStopAllEffect[INFO_LEN] = { 0 }; +wchar_t AudioEffectCtrlStopEffect[INFO_LEN] = { 0 }; +wchar_t AudioEffectCtrlVolume[INFO_LEN] = {0}; +//screen share +wchar_t screenShareCtrlScreenCap[INFO_LEN] = { 0 }; +wchar_t screenShareCtrlStartCap[INFO_LEN] = { 0 }; +wchar_t screenShareCtrlEndCap[INFO_LEN] = { 0 }; +wchar_t screenShareCtrlScreen[INFO_LEN] = { 0 }; +wchar_t screenShareCtrlShareSCreen[INFO_LEN] = { 0 }; +wchar_t screenShareCtrlStopShare[INFO_LEN] = { 0 }; +wchar_t screenShareCtrlGeneralSettings[INFO_LEN] = { 0 }; +wchar_t screenShareCtrlFPS[INFO_LEN] = { 0 }; +wchar_t screenShareCtrlBitrate[INFO_LEN] = { 0 }; +wchar_t screenShareCtrlShareCursor[INFO_LEN] = { 0 }; +wchar_t screenShareCtrlUpdateCaptureParam[INFO_LEN] = { 0 }; +wchar_t screenShareCtrlWindowFocus[INFO_LEN] = { 0 }; +wchar_t screenShareCtrlExcludeWindowList[INFO_LEN] = { 0 }; + + + +wchar_t screenCtrlRectInfo[INFO_LEN] = { 0 }; +wchar_t virtualScreenCtrlRectInfo[INFO_LEN] = { 0 }; +wchar_t screenCtrlCustomRectInfo[INFO_LEN] = { 0 }; + +//custom video capture +wchar_t customVideoCaptureCtrlCaptureVideoDevice[INFO_LEN] = { 0 }; +wchar_t customVideoCaptureCtrlSetExternlCapture[INFO_LEN] = { 0 }; +wchar_t customVideoCaptureCtrlCancelExternlCapture[INFO_LEN] = { 0 }; + +//original video process +wchar_t OriginalVideoCtrlProc[INFO_LEN] = { 0 }; +wchar_t OriginalVideoCtrlSetProc[INFO_LEN] = { 0 }; +wchar_t OriginalVideoCtrlUnSetProc[INFO_LEN] = { 0 }; + +//custom audio capture +wchar_t customAudioCaptureCtrlCaptureAudioDeivce[INFO_LEN] = { 0 }; +wchar_t customAudioCaptureCtrlSetExternlCapture[INFO_LEN] = { 0 }; +wchar_t customAudioCaptureCtrlCancelExternlCapture[INFO_LEN] = { 0 }; +extern wchar_t customAudioCaptureCtrlSetAudioRender[INFO_LEN] = { 0 }; +extern wchar_t customAudioCaptureCtrlCancelAudioRender[INFO_LEN] = { 0 }; + + +//original audio process +wchar_t OriginalAudioCtrlProc[INFO_LEN] = { 0 }; +wchar_t OriginalAudioCtrlSetProc[INFO_LEN] = { 0 }; +wchar_t OriginalAudioCtrlUnSetProc[INFO_LEN] = { 0 }; + +//media encrypt +wchar_t mediaEncryptCtrlMode[INFO_LEN] = { 0 }; +wchar_t mediaEncryptCtrlSecret[INFO_LEN] = { 0 }; +wchar_t mediaEncryptCtrlSetEncrypt[INFO_LEN] = { 0 }; +//custom encrypt +wchar_t customEncryptCtrlEncrypt[INFO_LEN] = { 0 }; +wchar_t customEncryptCtrlSetEncrypt[INFO_LEN] = { 0 }; +wchar_t customEncryptCtrlCancelEncrypt[INFO_LEN] = { 0 }; + + +//media player +wchar_t mediaPlayerCtrlVideoSource[INFO_LEN] = { 0 }; +wchar_t mediaPlayerCtrlOpen[INFO_LEN] = { 0 }; +wchar_t mediaPlayerCtrlClose[INFO_LEN] = { 0 }; +wchar_t mediaPlayerCtrlPause[INFO_LEN] = { 0 }; +wchar_t mediaPlayerCtrlPlay[INFO_LEN] = { 0 }; +wchar_t mediaPlayerCtrlAttachPlayer[INFO_LEN] = { 0 }; +wchar_t mediaPlayerCtrlDettachPlayer[INFO_LEN] = { 0 }; +wchar_t mediaPlayerCtrlPublishVideo[INFO_LEN] = { 0 }; +wchar_t mediaPlayerCtrlUnPublishVideo[INFO_LEN] = { 0 }; +wchar_t mediaPlayerCtrlPublishAudio[INFO_LEN] = { 0 }; +wchar_t mediaPlayerCtrlUnPublishAudio[INFO_LEN] = { 0 }; + +wchar_t MultiChannelCtrlChannelList[INFO_LEN] = {0}; + + + +//per call test +wchar_t PerCallTestCtrlAudioInput[INFO_LEN] = { 0 }; +wchar_t PerCallTestCtrlAudioOutput[INFO_LEN] = { 0 }; +wchar_t PerCallTestCtrlAudioVol[INFO_LEN] = { 0 }; +wchar_t PerCallTestCtrlCamera[INFO_LEN] = { 0 }; +wchar_t PerCallTestCtrlStartTest[INFO_LEN] = { 0 }; +wchar_t PerCallTestCtrlStopTest[INFO_LEN] = { 0 }; + +//audio volume +wchar_t AudioVolumeCtrlCapVol[INFO_LEN] = { 0 }; +wchar_t AudioVolumeCtrlCapSigVol[INFO_LEN] = { 0 }; +wchar_t AudioVolumeCtrlPlaybackVol[INFO_LEN] = { 0 }; +wchar_t AudioVolumeCtrlPlaybackSigVol[INFO_LEN] = { 0 }; + + + +//report in call +wchar_t ReportInCallCtrlGopTotal[INFO_LEN] = { 0 }; +wchar_t ReportInCallCtrlGopRemoteVideo[INFO_LEN] = { 0 }; +wchar_t ReportInCallCtrlGopRemoteAudio[INFO_LEN] = { 0 }; +wchar_t ReportInCallCtrlTotalUpDownLink[INFO_LEN] = { 0 }; +wchar_t ReportInCallCtrlTotalBytes[INFO_LEN] = { 0 }; +wchar_t ReportInCallCtrlTotalBitrate[INFO_LEN] = { 0 }; +wchar_t ReportInCallCtrlVideoNetWorkDelay[INFO_LEN] = { 0 }; +wchar_t ReportInCallCtrlVideoBytes[INFO_LEN] = { 0 }; +wchar_t ReportInCallCtrlVideoBitrate[INFO_LEN] = { 0 }; +wchar_t ReportInCallCtrlAudioNetWorkDelay[INFO_LEN] = { 0 }; +wchar_t ReportInCallCtrlAudioBytes[INFO_LEN] = { 0 }; +wchar_t ReportInCallCtrlAudioBitrate[INFO_LEN] = { 0 }; +wchar_t ReportInCallCtrlLocalResoultion[INFO_LEN] = { 0 }; +wchar_t ReportInCallCtrlLocalFPS[INFO_LEN] = { 0 }; + +wchar_t RegionConnCtrlAreaCode[INFO_LEN] = { 0 }; + + +//Cross Channel +wchar_t CrossChannelCtrlCrossChannel[INFO_LEN] = { 0 }; +wchar_t CrossChannelCtrlToken[INFO_LEN] = { 0 }; +wchar_t CrossChannelCtrlUid[INFO_LEN] = { 0 }; +wchar_t CrossChannelCrossChannelList[INFO_LEN] = { 0 }; +wchar_t CrossChannelAddChannel[INFO_LEN] = { 0 }; +wchar_t CrossChannelRemoveChannel[INFO_LEN] = { 0 }; +wchar_t CrossChannelStartMediaRelay[INFO_LEN] = { 0 }; +wchar_t CrossChannelStopMediaRelay[INFO_LEN] = { 0 }; +wchar_t CrossChannelUpdateMediaRelay[INFO_LEN] = { 0 }; + + +std::string cs2utf8(CString str) +{ + char szBuf[2 * MAX_PATH] = { 0 }; + WideCharToMultiByte(CP_UTF8, 0, str.GetBuffer(0), str.GetLength(), szBuf, 2 * MAX_PATH, NULL, NULL); + return szBuf; +} + +CString utf82cs(std::string utf8) +{ + TCHAR szBuf[2 * MAX_PATH] = { 0 }; + MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), 2 * MAX_PATH, szBuf, 2 * MAX_PATH); + return szBuf; +} + +CString getCurrentTime() +{ + SYSTEMTIME st = { 0 }; + GetLocalTime(&st); + CString strTime; + strTime.Format(_T("%02d:%02d:%02d %02d"), st.wHour, st.wHour, st.wMinute, st.wMilliseconds); + return strTime; +} + +BOOL PASCAL SaveResourceToFile(LPCTSTR lpResourceType, WORD wResourceID, LPCTSTR lpFilePath) +{ + HMODULE hModule = ::GetModuleHandle(NULL); + + if (hModule == NULL) + return FALSE; + + HRSRC hResrc = ::FindResource(hModule, MAKEINTRESOURCE(wResourceID), lpResourceType); + if (hResrc == NULL) + return FALSE; + + HGLOBAL hGlobal = ::LoadResource(hModule, hResrc); + if (hGlobal == NULL) + return FALSE; + + LPBYTE lpPointer = (LPBYTE)::LockResource(hGlobal); + DWORD dwResSize = ::SizeofResource(hModule, hResrc); + + HANDLE hFile = ::CreateFile(lpFilePath, GENERIC_ALL, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (hFile == INVALID_HANDLE_VALUE) + return FALSE; + + DWORD dwBytesWritten = 0; + ::WriteFile(hFile, lpPointer, dwResSize, &dwBytesWritten, NULL); + ::CloseHandle(hFile); + + return (dwBytesWritten == dwResSize) ? TRUE : FALSE; +} + +void InitKeyInfomation() +{ + //common + _tcscpy_s(commonGroupDoc, INFO_LEN, Str(_T("Common.Group.Doc"))); + _tcscpy_s(commonDocumentWebsite, INFO_LEN, Str(_T("Common.Document.Website"))); + _tcscpy_s(commonFAQWebsite, INFO_LEN, Str(_T("Common.FAQ.Website"))); + _tcscpy_s(commonRegisterWebsite, INFO_LEN, Str(_T("Common.Register.Website"))); + _tcscpy_s(commonDemoWebsite, INFO_LEN, Str(_T("Common.Demo.Website"))); + _tcscpy_s(commonBasicScene, INFO_LEN, Str(_T("Common.Basic.Scene"))); + _tcscpy_s(commonAdvanceScene, INFO_LEN, Str(_T("Common.Advanced.Scene"))); + + _tcscpy_s(commonCtrlChannel, INFO_LEN, Str(_T("Common.Ctrl.ChannelName"))); + _tcscpy_s(commonCtrlJoinChannel, INFO_LEN, Str(_T("Common.Ctrl.JoinChannel"))); + _tcscpy_s(commonCtrlLeaveChannel, INFO_LEN, Str(_T("Common.Ctrl.LeaveChannel"))); + _tcscpy_s(commonCtrlClientRole, INFO_LEN, Str(_T("Common.Ctrl.ClientRole"))); + _tcscpy_s(liveCtrlPersons, INFO_LEN, Str(_T("LiveBroadcasting.Ctrl.Persons"))); + + //basic scene list + _tcscpy_s(basicLiveBroadcasting, INFO_LEN, Str(_T("Basic.LiveBroadcasting"))); + //advanced scene list + _tcscpy_s(advancedRtmpInject, INFO_LEN, Str(_T("Advanced.RtmpInject"))); + _tcscpy_s(advancedRtmpStreaming, INFO_LEN, Str(_T("Advanced.RtmpStreaming"))); + _tcscpy_s(advancedVideoMetadata, INFO_LEN, Str(_T("Advanced.Metadata"))); + + _tcscpy_s(advancedMediaEncrypt, INFO_LEN, Str(_T("Advanced.MediaEncrypt"))); + _tcscpy_s(advancedCustomEncrypt, INFO_LEN, Str(_T("Advanced.CustomEncrypt"))); + + _tcscpy_s(advancedScreenCap, INFO_LEN, Str(_T("Advanced.ScreenCap"))); + _tcscpy_s(advancedBeauty, INFO_LEN, Str(_T("Advanced.Beauty"))); + _tcscpy_s(advancedBeautyAudio, INFO_LEN, Str(_T("Advanced.BeautyAudio"))); + _tcscpy_s(advancedVideoProfile, INFO_LEN, Str(_T("Advanced.VideoProfile"))); + + _tcscpy_s(advancedAudioProfile, INFO_LEN, Str(_T("Advanced.AudioProfile"))); + _tcscpy_s(advancedAudioMixing, INFO_LEN, Str(_T("Advanced.AudioMixing"))); + + + _tcscpy_s(advancedMediaIOCustomVideoCapture, INFO_LEN, Str(_T("Advanced.MediaIOVideoCapture"))); + + _tcscpy_s(advancedCustomVideoCapture, INFO_LEN, Str(_T("Advanced.CustomVideoCapture"))); + _tcscpy_s(advancedOriginalVideo, INFO_LEN, Str(_T("Advanced.OriginalVideo"))); + + _tcscpy_s(advancedCustomAudioCapture, INFO_LEN, Str(_T("Advanced.CustomAudioCapture"))); + _tcscpy_s(advancedOriginalAudio, INFO_LEN, Str(_T("Advanced.OriginalAudio"))); + _tcscpy_s(advancedMediaPlayer, INFO_LEN, Str(_T("Advanced.MediaPlayer"))); + _tcscpy_s(advancedAudioEffect, INFO_LEN, Str(_T("Advanced.AudioEffect"))); + _tcscpy_s(advancedMultiChannel, INFO_LEN, Str(_T("Advanced.MultiChannel"))); + _tcscpy_s(advancedPerCallTest, INFO_LEN, Str(_T("Advanced.PerCallTest"))); + _tcscpy_s(advancedAudioVolume, INFO_LEN, Str(_T("Advanced.AudioVolume"))); + _tcscpy_s(advancedReportInCall, INFO_LEN, Str(_T("Advanced.ReportInCall"))); + _tcscpy_s(advancedRegionConn, INFO_LEN, Str(_T("Advanced.RegionConn"))); + _tcscpy_s(advancedCrossChannel, INFO_LEN, Str(_T("Advanced.CrossChannel"))); + + + + + //agora + _tcscpy_s(agoraRoleBroadcaster, INFO_LEN, Str(_T("Agora.ClientRole.Broadcaster"))); + _tcscpy_s(agoraRoleAudience, INFO_LEN, Str(_T("Agora.ClientRole.Audience"))); + + + //rtmp streaming + _tcscpy_s(rtmpStreamingCtrlPublishUrl, INFO_LEN, Str(_T("RtmpStreaming.Ctrl.PublishUrl"))); + _tcscpy_s(rtmpStreamingCtrlAdd, INFO_LEN, Str(_T("RtmpStreaming.Ctrl.Add"))); + _tcscpy_s(rtmpStreamingCtrlRemove, INFO_LEN, Str(_T("RtmpStreaming.Ctrl.Remove"))); + _tcscpy_s(rtmpStreamingCtrlTransCoding, INFO_LEN, Str(_T("RtmpStreaming.Ctrl.TransCoding"))); + _tcscpy_s(rtmpStreamingCtrlRemoveAll, INFO_LEN, Str(_T("RtmpStreaming.Ctrl.RemoveAll"))); + //rtmp inject + _tcscpy_s(rtmpInjectCtrlUrl, INFO_LEN, Str(_T("RtmpInject.Ctrl.Url"))); + _tcscpy_s(rtmpInjectCtrlInject, INFO_LEN, Str(_T("RtmpInject.Ctrl.Inject"))); + _tcscpy_s(rtmpInjectCtrlRemove, INFO_LEN, Str(_T("RtmpInject.Ctrl.Remove"))); + //rtmp state changed + _tcscpy_s(agoraRtmpStateIdle, INFO_LEN, Str(_T("Agora.RtmpStateChange.IDLE"))); + _tcscpy_s(agoraRtmpStateConnecting, INFO_LEN, Str(_T("Agora.RtmpStateChange.Connecting"))); + _tcscpy_s(agoraRtmpStateRunning, INFO_LEN, Str(_T("Agora.RtmpStateChange.Running"))); + _tcscpy_s(agoraRtmpStateRunningSuccess, INFO_LEN, Str(_T("Agora.RtmpStateChange.Running.Success"))); + _tcscpy_s(agoraRtmpStateRecovering, INFO_LEN, Str(_T("Agora.RtmpStateChange.Recovering"))); + _tcscpy_s(agoraRtmpStateInvalidArg, INFO_LEN, Str(_T("Agora.RtmpStateChange.InvalidArg"))); + _tcscpy_s(agoraRtmpStateEncrypted, INFO_LEN, Str(_T("Agora.RtmpStateChange.Encrypted"))); + _tcscpy_s(agoraRtmpStateConnTimeout, INFO_LEN, Str(_T("Agora.RtmpStateChange.ConnTimeout"))); + _tcscpy_s(agoraRtmpStateInrealErr, INFO_LEN, Str(_T("Agora.RtmpStateChange.InrealErr"))); + _tcscpy_s(agoraRtmpStateServerErr, INFO_LEN, Str(_T("Agora.RtmpStateChange.ServerErr"))); + _tcscpy_s(agoraRtmpStateTooOften, INFO_LEN, Str(_T("Agora.RtmpStateChange.TooOften"))); + _tcscpy_s(agoraRtmpStateReachLimit, INFO_LEN, Str(_T("Agora.RtmpStateChange.ReachLimit"))); + _tcscpy_s(agoraRtmpStateNotAuth, INFO_LEN, Str(_T("Agora.RtmpStateChange.NotAuth"))); + _tcscpy_s(agoraRtmpStateNotFound, INFO_LEN, Str(_T("Agora.RtmpStateChange.NotFound"))); + _tcscpy_s(agoraRtmpStateNotSupported, INFO_LEN, Str(_T("Agora.RtmpStateChange.NotSupported"))); + + //inject status + _tcscpy_s(agoraInjectStartSucc, INFO_LEN, Str(_T("Agora.InjectStatus.StartSuccess"))); + _tcscpy_s(agoraInjectExist, INFO_LEN, Str(_T("Agora.InjectStatus.Exist"))); + _tcscpy_s(agoraInjectStartUnAuth, INFO_LEN, Str(_T("Agora.InjectStatus.StartUnAuth"))); + _tcscpy_s(agoraInjectStartTimeout, INFO_LEN, Str(_T("Agora.InjectStatus.StartTimeout"))); + _tcscpy_s(agoraInjectStartFailed, INFO_LEN, Str(_T("Agora.InjectStatus.StartFailed"))); + _tcscpy_s(agoraInjectStopSuccess, INFO_LEN, Str(_T("Agora.InjectStatus.StopSuccess"))); + _tcscpy_s(agoraInjectNotFound, INFO_LEN, Str(_T("Agora.InjectStatus.NotFound"))); + _tcscpy_s(agoraInjectStopUnAuth, INFO_LEN, Str(_T("Agora.InjectStatus.StopUnAuth"))); + _tcscpy_s(agoraInjectStopTimeout, INFO_LEN, Str(_T("Agora.InjectStatus.StopTimeout"))); + _tcscpy_s(agoraInjectStopFailed, INFO_LEN, Str(_T("Agora.InjectStatus.StopFailed"))); + _tcscpy_s(agoraInjectBroken, INFO_LEN, Str(_T("Agora.InjectStatus.Broken"))); + + + _tcscpy_s(videoSEIInformation, INFO_LEN, Str(_T("MetaData.Info"))); + _tcscpy_s(metadataCtrlSendSEI, INFO_LEN, Str(_T("MetaData.Ctrl.SendSEI"))); + _tcscpy_s(metadataCtrlBtnSend, INFO_LEN, Str(_T("MetaData.Ctrl.Send"))); + _tcscpy_s(metadataCtrlBtnClear, INFO_LEN, Str(_T("MetaData.Ctrl.Clear"))); + + + //screen share + _tcscpy_s(screenShareCtrlScreenCap, INFO_LEN, Str(_T("ScreenShare.Ctrl.ScreenCap"))); + _tcscpy_s(screenShareCtrlStartCap, INFO_LEN, Str(_T("ScreenShare.Ctrl.StartCap"))); + _tcscpy_s(screenShareCtrlEndCap, INFO_LEN, Str(_T("ScreenShare.Ctrl.EndCap"))); + _tcscpy_s(screenShareCtrlScreen, INFO_LEN, Str(_T("ScreenShare.Ctrl.Screen"))); + _tcscpy_s(screenShareCtrlShareSCreen, INFO_LEN, Str(_T("ScreenShare.Ctrl.ShareSCreen"))); + _tcscpy_s(screenShareCtrlStopShare, INFO_LEN, Str(_T("ScreenShare.Ctrl.StopShare"))); + _tcscpy_s(screenShareCtrlGeneralSettings, INFO_LEN, Str(_T("ScreenShare.Ctrl.GeneralSettings"))); + _tcscpy_s(screenShareCtrlFPS, INFO_LEN, Str(_T("ScreenShare.Ctrl.FPS"))); + _tcscpy_s(screenShareCtrlBitrate, INFO_LEN, Str(_T("ScreenShare.Ctrl.Bitrate"))); + _tcscpy_s(screenShareCtrlShareCursor, INFO_LEN, Str(_T("ScreenShare.Ctrl.ShareCursor"))); + _tcscpy_s(screenShareCtrlUpdateCaptureParam, INFO_LEN, Str(_T("ScreenShare.Ctrl.UpdateCaptureParam"))); + _tcscpy_s(screenShareCtrlWindowFocus, INFO_LEN, Str(_T("ScreenShare.Ctrl.WindowFocus"))); + _tcscpy_s(screenShareCtrlExcludeWindowList, INFO_LEN, Str(_T("ScreenShare.Ctrl.ExcludeWindowList"))); + + _tcscpy_s(screenCtrlRectInfo, INFO_LEN, Str(_T("Share.Ctrl.Screen.RectInfo"))); + _tcscpy_s(virtualScreenCtrlRectInfo, INFO_LEN, Str(_T("Share.Ctrl.VirtualScreen.RectInfo"))); + _tcscpy_s(screenCtrlCustomRectInfo, INFO_LEN, Str(_T("Share.Ctrl.Screen.CustomInfo"))); + + //beauty + _tcscpy_s(beautyCtrlLighteningContrastLevel, INFO_LEN, Str(_T("Beauty.Ctrl.LighteningContrastLevel"))); + _tcscpy_s(beautyCtrlLightening, INFO_LEN, Str(_T("Beauty.Ctrl.Lightening"))); + _tcscpy_s(beautyCtrlRedness, INFO_LEN, Str(_T("Beauty.Ctrl.Redness"))); + _tcscpy_s(beautyCtrlSmoothness, INFO_LEN, Str(_T("Beauty.Ctrl.Smoothness"))); + _tcscpy_s(beautyCtrlEnable, INFO_LEN, Str(_T("Beauty.Ctrl.Enable"))); + + + //beauty audio + _tcscpy_s(beautyAudioCtrlChange, INFO_LEN, Str(_T("BeautyAudio.Ctrl.Change"))); + _tcscpy_s(beautyAudioCtrlSetAudioChange, INFO_LEN, Str(_T("BeautyAudio.Ctrl.SetAudioChange"))); + _tcscpy_s(beautyAudioCtrlUnSetAudioChange, INFO_LEN, Str(_T("BeautyAudio.Ctrl.UnSetAudioChange"))); + _tcscpy_s(beautyAudioCtrlPreSet, INFO_LEN, Str(_T("BeautyAudio.Ctrl.ReverbPreSet"))); + _tcscpy_s(beautyAudioCtrlParam1, INFO_LEN, Str(_T("BeautyAudio.Ctrl.BeautyAudioCtrlParam1"))); + _tcscpy_s(beautyAudioCtrlParam2, INFO_LEN, Str(_T("BeautyAudio.Ctrl.BeautyAudioCtrlParam2"))); + + + //video profile + _tcscpy_s(videoProfileCtrldegradationPreference, INFO_LEN, Str(_T("VideoProfile.Ctrl.DegradationPreference"))); + _tcscpy_s(videoProfileCtrlFPS, INFO_LEN, Str(_T("VideoProfile.Ctrl.FPS"))); + _tcscpy_s(videoProfileCtrlHeight, INFO_LEN, Str(_T("VideoProfile.Ctrl.Height"))); + _tcscpy_s(videoProfileCtrlWidth, INFO_LEN, Str(_T("VideoProfile.Ctrl.Width"))); + _tcscpy_s(videoProfileCtrlBitrate, INFO_LEN, Str(_T("VideoProfile.Ctrl.Bitrate"))); + _tcscpy_s(videoProfileCtrlUnSetVideoProfile, INFO_LEN, Str(_T("VideoProfile.Ctrl.UnSetVideoProfile"))); + _tcscpy_s(videoProfileCtrlSetVideoProfile, INFO_LEN, Str(_T("VideoProfile.Ctrl.SetVideoProfile"))); + + //audio profile + _tcscpy_s(audioProfileCtrlSetAudioProfile, INFO_LEN, Str(_T("AudioProfile.Ctrl.SetAudioProfile"))); + _tcscpy_s(audioProfileCtrlProfile, INFO_LEN, Str(_T("AudioProfile.Ctrl.Profile"))); + _tcscpy_s(audioProfileCtrlScenario, INFO_LEN, Str(_T("AudioProfile.Ctrl.Scenario"))); + _tcscpy_s(audioProfileCtrlUnSetAudioProfile, INFO_LEN, Str(_T("AudioProfile.Ctrl.UnSetAudioProfile"))); + + //audio mixing + _tcscpy_s(audioMixingCtrlMixingPath, INFO_LEN, Str(_T("AudioMixing.Ctrl.MixingPath"))); + _tcscpy_s(audioMixingCtrlOnlyLocal, INFO_LEN, Str(_T("AudioMixing.Ctrl.OnlyLocal"))); + _tcscpy_s(audioMixingCtrlSetAudioMixing, INFO_LEN, Str(_T("AudioMixing.Ctrl.SetAudioMixing"))); + _tcscpy_s(audioMixingCtrlRepeatTimes, INFO_LEN, Str(_T("AudioMixing.Ctrl.RepeatTimes"))); + _tcscpy_s(audioMixingCtrlUnSetAudioMixing, INFO_LEN, Str(_T("AudioMixing.Ctrl.UnSetAudioMixing"))); + _tcscpy_s(audioMixingCtrlReplaceMicroPhone, INFO_LEN, Str(_T("AudioMixing.Ctrl.ReplaceMicroPhone"))); + + //audio effect + _tcscpy_s(AudioEffectCtrlEffectPath, INFO_LEN, Str(_T("AudioEffect.Ctrl.EffectPath"))); + _tcscpy_s(AudioEffectCtrlEffect, INFO_LEN, Str(_T("AudioEffect.Ctrl.Effect"))); + _tcscpy_s(AudioEffectCtrlLoops, INFO_LEN, Str(_T("AudioEffect.Ctrl.Loops"))); + _tcscpy_s(AudioEffectCtrlGain, INFO_LEN, Str(_T("AudioEffect.Ctrl.Gain"))); + _tcscpy_s(AudioEffectCtrlPitch, INFO_LEN, Str(_T("AudioEffect.Ctrl.Pitch"))); + _tcscpy_s(AudioEffectCtrlPan, INFO_LEN, Str(_T("AudioEffect.Ctrl.Pan"))); + _tcscpy_s(AudioEffectCtrlPublish, INFO_LEN, Str(_T("AudioEffect.Ctrl.Publish"))); + _tcscpy_s(AudioEffectCtrlAddEffect, INFO_LEN, Str(_T("AudioEffect.Ctrl.AddEffect"))); + _tcscpy_s(AudioEffectCtrlRemoveEffect, INFO_LEN, Str(_T("AudioEffect.Ctrl.RemoveEffect"))); + _tcscpy_s(AudioEffectCtrlPreLoad, INFO_LEN, Str(_T("AudioEffect.Ctrl.PreLoad"))); + _tcscpy_s(AudioEffectCtrlUnPreload, INFO_LEN, Str(_T("AudioEffect.Ctrl.UnPreload"))); + _tcscpy_s(AudioEffectCtrlPauseEffect, INFO_LEN, Str(_T("AudioEffect.Ctrl.PauseEffect"))); + _tcscpy_s(AudioEffectCtrlPlayEffect, INFO_LEN, Str(_T("AudioEffect.Ctrl.PlayEffect"))); + _tcscpy_s(AudioEffectCtrlPauseAllEffect, INFO_LEN, Str(_T("AudioEffect.Ctrl.PauseAllEffect"))); + _tcscpy_s(AudioEffectCtrlResumeEffect, INFO_LEN, Str(_T("AudioEffect.Ctrl.ResumeEffect"))); + _tcscpy_s(AudioEffectCtrlResumeAllEffect, INFO_LEN, Str(_T("AudioEffect.Ctrl.ResumeAllEffect"))); + _tcscpy_s(AudioEffectCtrlStopAllEffect, INFO_LEN, Str(_T("AudioEffect.Ctrl.StopAllEffect"))); + _tcscpy_s(AudioEffectCtrlStopEffect, INFO_LEN, Str(_T("AudioEffect.Ctrl.StopEffect"))); + _tcscpy_s(AudioEffectCtrlVolume, INFO_LEN, Str(_T("AudioEffect.Ctrl.Volume"))); + + + //custom video capture + _tcscpy_s(customVideoCaptureCtrlCaptureVideoDevice, INFO_LEN, Str(_T("CustomVideoCapture.Ctrl.CaptureVideo"))); + _tcscpy_s(customVideoCaptureCtrlSetExternlCapture, INFO_LEN, Str(_T("CustomVideoCapture.Ctrl.SetExternlCap"))); + _tcscpy_s(customVideoCaptureCtrlCancelExternlCapture, INFO_LEN, Str(_T("CustomVideoCapture.Ctrl.CancelExternlCap"))); + + //original video process + _tcscpy_s(OriginalVideoCtrlProc, INFO_LEN, Str(_T("OriginalVideo.Ctrl.Proc"))); + _tcscpy_s(OriginalVideoCtrlSetProc, INFO_LEN, Str(_T("OriginalVideo.Ctrl.SetProc"))); + _tcscpy_s(OriginalVideoCtrlUnSetProc, INFO_LEN, Str(_T("OriginalVideo.Ctrl.UnSetProc"))); + + //custom audio capture + _tcscpy_s(customAudioCaptureCtrlCaptureAudioDeivce, INFO_LEN, Str(_T("CustomAudioCapture.Ctrl.CaptureAudio"))); + _tcscpy_s(customAudioCaptureCtrlSetExternlCapture, INFO_LEN, Str(_T("CustomAudioCapture.Ctrl.SetExternlCap"))); + _tcscpy_s(customAudioCaptureCtrlCancelExternlCapture, INFO_LEN, Str(_T("CustomAudioCapture.Ctrl.CancelExternlCap"))); + _tcscpy_s(customAudioCaptureCtrlSetAudioRender, INFO_LEN, Str(_T("CustomAudioCapture.Ctrl.SetAudioRender"))); + _tcscpy_s(customAudioCaptureCtrlCancelAudioRender, INFO_LEN, Str(_T("CustomAudioCapture.Ctrl.CancelAudioRender"))); + + + //original video process + _tcscpy_s(OriginalAudioCtrlProc, INFO_LEN, Str(_T("OriginalVideo.Ctrl.Proc"))); + _tcscpy_s(OriginalAudioCtrlSetProc, INFO_LEN, Str(_T("OriginalVideo.Ctrl.SetProc"))); + _tcscpy_s(OriginalAudioCtrlUnSetProc, INFO_LEN, Str(_T("OriginalVideo.Ctrl.UnSetProc"))); + + //custom encrypt + _tcscpy_s(customEncryptCtrlEncrypt, INFO_LEN, Str(_T("CustomEncrypt.Ctrl.Encrypt"))); + _tcscpy_s(customEncryptCtrlSetEncrypt, INFO_LEN, Str(_T("CustomEncrypt.Ctrl.SetEncrypt"))); + _tcscpy_s(customEncryptCtrlCancelEncrypt, INFO_LEN, Str(_T("CustomEncrypt.Ctrl.CancelEncrypt"))); + + //custom encrypt + _tcscpy_s(mediaEncryptCtrlMode, INFO_LEN, Str(_T("MediaEncrypt.Ctrl.Mode"))); + _tcscpy_s(mediaEncryptCtrlSecret, INFO_LEN, Str(_T("MediaEncrypt.Ctrl.Secret"))); + _tcscpy_s(mediaEncryptCtrlSetEncrypt, INFO_LEN, Str(_T("MediaEncrypt.Ctrl.SetEncrypt"))); + + //media player + _tcscpy_s(mediaPlayerCtrlVideoSource, INFO_LEN, Str(_T("mediaPlayer.Ctrl.VideoSource"))); + _tcscpy_s(mediaPlayerCtrlOpen, INFO_LEN, Str(_T("mediaPlayer.Ctrl.Open"))); + _tcscpy_s(mediaPlayerCtrlClose, INFO_LEN, Str(_T("mediaPlayer.Ctrl.Close"))); + _tcscpy_s(mediaPlayerCtrlPause, INFO_LEN, Str(_T("mediaPlayer.Ctrl.Pause"))); + _tcscpy_s(mediaPlayerCtrlPlay, INFO_LEN, Str(_T("mediaPlayer.Ctrl.Play"))); + _tcscpy_s(mediaPlayerCtrlAttachPlayer, INFO_LEN, Str(_T("mediaPlayer.Ctrl.AttachPlayer"))); + _tcscpy_s(mediaPlayerCtrlDettachPlayer, INFO_LEN, Str(_T("mediaPlayer.Ctrl.DettachPlayer"))); + _tcscpy_s(mediaPlayerCtrlPublishVideo, INFO_LEN, Str(_T("mediaPlayer.Ctrl.PublishVideo"))); + _tcscpy_s(mediaPlayerCtrlUnPublishVideo, INFO_LEN, Str(_T("mediaPlayer.Ctrl.UnPublishVideo"))); + _tcscpy_s(mediaPlayerCtrlPublishAudio, INFO_LEN, Str(_T("mediaPlayer.Ctrl.PublishAudio"))); + _tcscpy_s(mediaPlayerCtrlUnPublishAudio, INFO_LEN, Str(_T("mediaPlayer.Ctrl.UnPublishAudio"))); + + _tcscpy_s(MultiChannelCtrlChannelList, INFO_LEN, Str(_T("MultiChannel.Ctrl.ChannelList"))); + + _tcscpy_s(PerCallTestCtrlAudioInput, INFO_LEN, Str(_T("PerCallTest.Ctrl.AudioInput"))); + _tcscpy_s(PerCallTestCtrlAudioOutput, INFO_LEN, Str(_T("PerCallTest.Ctrl.AudioOutput"))); + _tcscpy_s(PerCallTestCtrlAudioVol, INFO_LEN, Str(_T("PerCallTest.Ctrl.AudioVol"))); + _tcscpy_s(PerCallTestCtrlCamera, INFO_LEN, Str(_T("PerCallTest.Ctrl.Camera"))); + _tcscpy_s(PerCallTestCtrlStartTest, INFO_LEN, Str(_T("PerCallTest.Ctrl.StartTest"))); + _tcscpy_s(PerCallTestCtrlStopTest, INFO_LEN, Str(_T("PerCallTest.Ctrl.StopTest"))); + + _tcscpy_s(AudioVolumeCtrlCapVol, INFO_LEN, Str(_T("AudioVolume.Ctrl.AudioCapVol"))); + _tcscpy_s(AudioVolumeCtrlCapSigVol, INFO_LEN, Str(_T("AudioVolume.Ctrl.AudioCapSigVol"))); + _tcscpy_s(AudioVolumeCtrlPlaybackVol, INFO_LEN, Str(_T("AudioVolume.Ctrl.AudioPlaybackVol"))); + _tcscpy_s(AudioVolumeCtrlPlaybackSigVol, INFO_LEN, Str(_T("AudioVolume.Ctrl.AudioPlaybackSigVol"))); + + _tcscpy_s(ReportInCallCtrlLocalFPS, INFO_LEN, Str(_T("ReportInCall.Ctrl.LocalFPS"))); + _tcscpy_s(ReportInCallCtrlLocalResoultion, INFO_LEN, Str(_T("ReportInCall.Ctrl.LocaLResoultion"))); + _tcscpy_s(ReportInCallCtrlAudioBitrate, INFO_LEN, Str(_T("ReportInCall.Ctrl.AudioBitrate"))); + _tcscpy_s(ReportInCallCtrlAudioBytes, INFO_LEN, Str(_T("ReportInCall.Ctrl.AudioBytes"))); + _tcscpy_s(ReportInCallCtrlAudioNetWorkDelay, INFO_LEN, Str(_T("ReportInCall.Ctrl.AudioNetWorkDelay"))); + _tcscpy_s(ReportInCallCtrlGopRemoteAudio, INFO_LEN, Str(_T("ReportInCall.Ctrl.GopRemoteAudio"))); + _tcscpy_s(ReportInCallCtrlGopRemoteVideo, INFO_LEN, Str(_T("ReportInCall.Ctrl.GopRemoteVideo"))); + _tcscpy_s(ReportInCallCtrlGopTotal, INFO_LEN, Str(_T("ReportInCall.Ctrl.GopTotal"))); + _tcscpy_s(ReportInCallCtrlTotalBitrate, INFO_LEN, Str(_T("ReportInCall.Ctrl.TotalBitrate"))); + _tcscpy_s(ReportInCallCtrlTotalBytes, INFO_LEN, Str(_T("ReportInCall.Ctrl.TotalBytes"))); + _tcscpy_s(ReportInCallCtrlTotalUpDownLink, INFO_LEN, Str(_T("ReportInCall.Ctrl.TotalUpDownLink"))); + _tcscpy_s(ReportInCallCtrlVideoNetWorkDelay, INFO_LEN, Str(_T("ReportInCall.Ctrl.VideoNetWorkDelay"))); + _tcscpy_s(ReportInCallCtrlVideoBitrate, INFO_LEN, Str(_T("ReportInCall.Ctrl.VideoBitrate"))); + _tcscpy_s(ReportInCallCtrlVideoBytes, INFO_LEN, Str(_T("ReportInCall.Ctrl.VideoBytes"))); + + _tcscpy_s(RegionConnCtrlAreaCode, INFO_LEN, Str(_T("RegionConn.Ctrl.AreaCode"))); + + _tcscpy_s(CrossChannelAddChannel, INFO_LEN, Str(_T("CrossChannel.Ctrl.AddChannel"))); + _tcscpy_s(CrossChannelCrossChannelList, INFO_LEN, Str(_T("CrossChannel.Ctrl.CrossChannelList"))); + _tcscpy_s(CrossChannelCtrlCrossChannel, INFO_LEN, Str(_T("CrossChannel.Ctrl.CrossChannel"))); + _tcscpy_s(CrossChannelCtrlToken, INFO_LEN, Str(_T("CrossChannel.Ctrl.Token"))); + _tcscpy_s(CrossChannelCtrlUid, INFO_LEN, Str(_T("CrossChannel.Ctrl.Uid"))); + _tcscpy_s(CrossChannelRemoveChannel, INFO_LEN, Str(_T("CrossChannel.Ctrl.RemoveChannel"))); + _tcscpy_s(CrossChannelStartMediaRelay, INFO_LEN, Str(_T("CrossChannel.Ctrl.StartMediaRelay"))); + _tcscpy_s(CrossChannelStopMediaRelay, INFO_LEN, Str(_T("CrossChannel.Ctrl.StopMediaRelay"))); + _tcscpy_s(CrossChannelUpdateMediaRelay, INFO_LEN, Str(_T("CrossChannel.Ctrl.UpdateMediaRelay"))); + + + + /* + + + _tcscpy_s(, INFO_LEN, Str(_T(""))); + + _tcscpy_s(, INFO_LEN, Str(_T(""))); + _tcscpy_s(, INFO_LEN, Str(_T(""))); + _tcscpy_s(, INFO_LEN, Str(_T(""))); + _tcscpy_s(, INFO_LEN, Str(_T(""))); + _tcscpy_s(, INFO_LEN, Str(_T(""))); + _tcscpy_s(, INFO_LEN, Str(_T(""))); + _tcscpy_s(, INFO_LEN, Str(_T(""))); + _tcscpy_s(, INFO_LEN, Str(_T(""))); + _tcscpy_s(, INFO_LEN, Str(_T(""))); + _tcscpy_s(, INFO_LEN, Str(_T(""))); + _tcscpy_s(, INFO_LEN, Str(_T(""))); + _tcscpy_s(, INFO_LEN, Str(_T(""))); +*/ +} diff --git a/windows/APIExample/APIExample/stdafx.h b/windows/APIExample/APIExample/stdafx.h new file mode 100644 index 000000000..8a0d0c745 --- /dev/null +++ b/windows/APIExample/APIExample/stdafx.h @@ -0,0 +1,118 @@ + +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, +// but are changed infrequently + +#pragma once + +#ifndef VC_EXTRALEAN +#define VC_EXTRALEAN // Exclude rarely-used stuff from Windows headers +#endif + +#include "targetver.h" + +#define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS // some CString constructors will be explicit + +// turns off MFC's hiding of some common and often safely ignored warning messages +#define _AFX_ALL_WARNINGS + +#include // MFC core and standard components +#include // MFC extensions + + +#include // MFC Automation classes + + + +#ifndef _AFX_NO_OLE_SUPPORT +#include // MFC support for Internet Explorer 4 Common Controls +#endif +#ifndef _AFX_NO_AFXCMN_SUPPORT +#include // MFC support for Windows Common Controls +#endif // _AFX_NO_AFXCMN_SUPPORT + +#include // MFC support for ribbons and control bars + +#include + +#include +#pragma comment(lib, "Shcore.lib") + +#pragma warning(disable:4819) + +#define APP_ID "" + + +#define APP_TOKEN "" + +#include +#include +#include + +#include +#include "CConfig.h" +#include "Language.h" +#include +#include + +#pragma comment(lib, "agora_rtc_sdk.lib") +using namespace agora; +using namespace agora::rtc; +using namespace agora::media; +#define WM_MSGID(code) (WM_USER+0x200+code) +//Agora Event Handler Message and structure +#define EID_JOINCHANNEL_SUCCESS 0x00000001 +#define EID_LEAVE_CHANNEL 0x00000002 +#define EID_USER_JOINED 0x00000003 +#define EID_USER_OFFLINE 0x00000004 +#define EID_INJECT_STATUS 0x00000005 +#define EID_RTMP_STREAM_STATE_CHANGED 0x00000006 +#define EID_REMOTE_VIDEO_STATE_CHANED 0x00000007 +#define RECV_METADATA_MSG 0x00000008 +#define mediaPLAYER_STATE_CHANGED 0x00000009 +#define mediaPLAYER_POSTION_CHANGED 0x0000000A +#define EID_LOCAL_VIDEO_STATE_CHANGED 0x0000000B +#define EID_LASTMILE_QUAILTY 0x0000000C +#define EID_LASTMILE_PROBE_RESULT 0x0000000D +#define EID_AUDIO_VOLUME_INDICATION 0x0000000E +#define EID_AUDIO_ACTIVE_SPEAKER 0x0000000F +#define EID_RTC_STATS 0x00000010 +#define EID_REMOTE_AUDIO_STATS 0x00000011 +#define EID_REMOTE_VIDEO_STATS 0x00000012 +#define EID_LOCAL_VIDEO_STATS 0x00000013 +#define EID_CHANNEL_MEDIA_RELAY_STATE_CHNAGENED 0x00000014 +#define EID_CHANNEL_MEDIA_RELAY_EVENT 0x00000015 + + + +typedef struct _tagRtmpStreamStateChanged { + char* url; + int state; + int error; +}RtmpStreamStreamStateChanged, *PRtmpStreamStreamStateChanged; + +typedef struct _tagVideoStateStateChanged { + uid_t uid; + REMOTE_VIDEO_STATE state; + REMOTE_VIDEO_STATE_REASON reason; +}VideoStateStateChanged, *PVideoStateStateChanged; + +std::string cs2utf8(CString str); +CString utf82cs(std::string utf8); +CString getCurrentTime(); +BOOL PASCAL SaveResourceToFile(LPCTSTR lpResourceType, WORD wResourceID, LPCTSTR lpFilePath); + + +#define ID_BASEWND_VIDEO 20000 +#define MAIN_AREA_TOP 20 +#ifdef _UNICODE +#if defined _M_IX86 +#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='x86' publicKeyToken='6595b64144ccf1df' language='*'\"") +#elif defined _M_X64 +#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='amd64' publicKeyToken='6595b64144ccf1df' language='*'\"") +#else +#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") +#endif +#endif + + diff --git a/windows/APIExample/APIExample/targetver.h b/windows/APIExample/APIExample/targetver.h new file mode 100644 index 000000000..87c0086de --- /dev/null +++ b/windows/APIExample/APIExample/targetver.h @@ -0,0 +1,8 @@ +#pragma once + +// Including SDKDDKVer.h defines the highest available Windows platform. + +// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and +// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. + +#include diff --git a/windows/APIExample/APIExample/zh-cn.ini b/windows/APIExample/APIExample/zh-cn.ini new file mode 100644 index 000000000..1036e818d --- /dev/null +++ b/windows/APIExample/APIExample/zh-cn.ini @@ -0,0 +1,253 @@ +[General] +Common.Group.Doc=ĵϢ +Common.Document.Website=ĵ +Common.FAQ.Website=ʴ +Common.Register.Website=ע˻ +Common.Demo.Website=demoַ +Common.Basic.Scene= +Common.Advanced.Scene=׳ + + +Agora.ClientRole.Broadcaster= +Agora.ClientRole.Audience= + +Basic.LiveBroadcasting=ֱ +Advanced.RtmpStreaming=· +Advanced.RtmpInject=ý +Advanced.Metadata=ƵSEI +Advanced.CustomEncryp=Զ +Advanced.Beauty= +Advanced.BeautyAudio= +Advanced.AudioMixing= +Advanced.VideoProfile=Ƶ +Advanced.AudioProfile=Ƶ +Advanced.ScreenCap=Ļ +Advanced.MediaIOVideoCapture=media ioԶƵɼ +Advanced.CustomVideoCapture=ԶƵɼ +Advanced.OriginalVideo=ԭʼƵ +Advanced.OriginalAudio=ԭʼƵ +Advanced.CustomAudioCapture=ԶƵɼ +Advanced.MediaEncrypt=ܴ +Advanced.CustomEncrypt=Զ +Advanced.MediaPlayer=ý岥 +Advanced.AudioEffect=Ч +Advanced.MultiChannel=Ƶ +Advanced.PerCallTest=Ƶǰ +Advanced.AudioVolume=Ƶ +Advanced.ReportInCall=ͨв +Advanced.RegionConn= +Advanced.CrossChannel=Ƶ + +Common.Ctrl.ChannelName=Ƶ +Common.Ctrl.JoinChannel=Ƶ +Common.Ctrl.LeaveChannel=뿪Ƶ +Common.Ctrl.ClientRole=ɫ + +LiveBroadcasting.Ctrl.Persons= + + +RtmpStreaming.Ctrl.TransCoding=ת +RtmpInject.Ctrl.Url=ַ +RtmpInject.Ctrl.Inject= +RtmpInject.Ctrl.Remove=Ƴ +Agora.InjectStatus.StartSuccess=ⲿƵɹ +Agora.InjectStatus.Exist=ⲿƵѴ +Agora.InjectStatus.StartUnAuth=ⲿƵδȨ +Agora.InjectStatus.StartTimeout=ⲿƵʱ +Agora.InjectStatus.StartFailed=ⲿƵʧ +Agora.InjectStatus.StopSuccess=ⲿƵֹͣɹ +Agora.InjectStatus.NotFound=δҵҪֹͣⲿƵ +Agora.InjectStatus.StopUnAuth=ҪֹͣⲿƵδȨ +Agora.InjectStatus.StopTimeout=ֹͣⲿƵʱ +Agora.InjectStatus.StopFailed=ֹͣⲿƵʧ +Agora.InjectStatus.Broken=ⲿƵж + +RtmpStreaming.Ctrl.PublishUrl=ַ +RtmpStreaming.Ctrl.Add= +RtmpStreaming.Ctrl.Remove=Ƴ +RtmpStreaming.Ctrl.RemoveAll=Ƴ +Agora.RtmpStateChange.IDLE=ɹɾַ +Agora.RtmpStateChange.Connecting= Agora RTMP +Agora.RtmpStateChange.Running=ڽС +Agora.RtmpStateChange.Running.Success=ɹ +Agora.RtmpStateChange.Recovering=ڻָ CDN 쳣жʱSDK ԶԻָظ״̬ +Agora.RtmpStateChange.InvalidArg=Ч +Agora.RtmpStateChange.Encrypted=Ѽܣ +Agora.RtmpStateChange.ConnTimeout=ʱδɹ +Agora.RtmpStateChange.InrealErr=ִ +Agora.RtmpStateChange.ServerErr=RTMP ִ +Agora.RtmpStateChange.TooOften=Ƶ +Agora.RtmpStateChange.ReachLimit=ַĿﵽ 10ɾһЩõַַ +Agora.RtmpStateChange.NotAuth=Լ +Agora.RtmpStateChange.NotFound=δҵ +Agora.RtmpStateChange.NotSupported=ַʽдַʽǷȷ + +MetaData.Info=֧1024ֽ +MetaData.Ctrl.SendSEI=SEIϢ +MetaData.Ctrl.Send= +MetaData.Ctrl.Clear= + +ScreenShare.Ctrl.ScreenCap=ھ +ScreenShare.Ctrl.StartCap= +ScreenShare.Ctrl.EndCap=ֹͣ +ScreenShare.Ctrl.Screen=Ļ +ScreenShare.Ctrl.ShareSCreen=Ļ +ScreenShare.Ctrl.StopShare=ֹͣ +ScreenShare.Ctrl.GeneralSettings= +ScreenShare.Ctrl.FPS=֡ +ScreenShare.Ctrl.Bitrate= +ScreenShare.Ctrl.ShareCursor=ָ +ScreenShare.Ctrl.UpdateCaptureParam=²ɼ +ScreenShare.Ctrl.WindowFocus=ý +ScreenShare.Ctrl.ExcludeWindowList=δб + +Share.Ctrl.Screen.RectInfo=Ļ +Share.Ctrl.VirtualScreen.RectInfo=Ļ +Share.Ctrl.Screen.CustomInfo=Զ + +CustomVideoCapture.Ctrl.CaptureVideo =ɼƵ豸 +CustomVideoCapture.Ctrl.SetExternlCap=Ƶɼ +CustomVideoCapture.Ctrl.CancelExternlCap=ȡƵɼ + +CustomAudioCapture.Ctrl.CaptureAudio = ɼƵ豸 +CustomAudioCapture.Ctrl.SetExternlCap = Ƶɼ +CustomAudioCapture.Ctrl.CancelExternlCap = ȡƵɼ +CustomAudioCapture.Ctrl.SetAudioRender = ƵȾ +CustomAudioCapture.Ctrl.CancelAudioRender = ȡƵȾ + +Beauty.Ctrl.LighteningContrastLevel= Աȶ +Beauty.Ctrl.Lightening = (0~10) +Beauty.Ctrl.Smoothness = ƽ(0~10) +Beauty.Ctrl.Redness= (0~10) +Beauty.Ctrl.Enable= + + +VideoProfile.Ctrl.DegradationPreference = Ͳ +VideoProfile.Ctrl.FPS = ֡ +VideoProfile.Ctrl.Height = ߶ +VideoProfile.Ctrl.Width = +VideoProfile.Ctrl.Bitrate = +VideoProfile.Ctrl.UnSetVideoProfile = ȡ +VideoProfile.Ctrl.SetVideoProfile = Ƶ + + +AudioProfile.Ctrl.UnSetAudioProfile=ȡ +AudioProfile.Ctrl.SetAudioProfile=Ƶ +AudioProfile.Ctrl.Profile= +AudioProfile.Ctrl.Scenario= + +BeautyAudio.Ctrl.SetAudioChange= +BeautyAudio.Ctrl.UnSetAudioChange=ȡ +BeautyAudio.Ctrl.Change= +BeautyAudio.Ctrl.ReverbPreSet=Ч +BeautyAudio.Ctrl.BeautyAudioCtrlParam1=1 +BeautyAudio.Ctrl.BeautyAudioCtrlParam2=2 + + +AudioMixing.Ctrl.MixingPath = · +AudioMixing.Ctrl.RepeatTimes = ظ +AudioMixing.Ctrl.SetAudioMixing=û +AudioMixing.Ctrl.UnSetAudioMixing=ȡ +AudioMixing.Ctrl.OnlyLocal=ز +AudioMixing.Ctrl.ReplaceMicroPhone=滻˷ + + +AudioEffect.Ctrl.EffectPath=Ч· +AudioEffect.Ctrl.Effect=Ч +AudioEffect.Ctrl.Loops=Ŵ +AudioEffect.Ctrl.Gain= +AudioEffect.Ctrl.Pitch= +AudioEffect.Ctrl.Pan=ռ +AudioEffect.Ctrl.Publish= +AudioEffect.Ctrl.AddEffect=Ч +AudioEffect.Ctrl.RemoveEffect=ƳЧ +AudioEffect.Ctrl.PreLoad=ԤЧ +AudioEffect.Ctrl.UnPreload=жЧ +AudioEffect.Ctrl.PauseEffect=ͣЧ +AudioEffect.Ctrl.PlayEffect=Ч +AudioEffect.Ctrl.PauseAllEffect=ͣЧ +AudioEffect.Ctrl.ResumeEffect=ָЧ +AudioEffect.Ctrl.ResumeAllEffect=ָЧ +AudioEffect.Ctrl.StopAllEffect=ֹͣЧ +AudioEffect.Ctrl.StopEffect=ֹͣЧ +AudioEffect.Ctrl.Volume= + +OriginalVideo.Ctrl.Proc = ԭʼƵ +OriginalVideo.Ctrl.SetProc = ô +OriginalVideo.Ctrl.UnSetProc = ȡ + + +OriginalAudio.Ctrl.Proc = ԭʼƵ +OriginalAudio.Ctrl.SetProc = ô +OriginalAudio.Ctrl.UnSetProc = ȡ + + +CustomEncrypt.Ctrl.Encrypt=ܷʽ +CustomEncrypt.Ctrl.SetEncrypt=ü +CustomEncrypt.Ctrl.CancelEncrypt=ȡ + +MediaEncrypt.Ctrl.Mode=ģʽ +MediaEncrypt.Ctrl.Secret= +MediaEncrypt.Ctrl.SetEncrypt=ü + +mediaPlayer.Ctrl.VideoSource=ýַ +mediaPlayer.Ctrl.Open= +mediaPlayer.Ctrl.Close=ֹͣ +mediaPlayer.Ctrl.Pause=ͣ +mediaPlayer.Ctrl.Play= +mediaPlayer.Ctrl.AttachPlayer=Ƶ +mediaPlayer.Ctrl.DettachPlayer=ȡ +mediaPlayer.Ctrl.PublishVideo=Ƶ +mediaPlayer.Ctrl.UnPublishVideo=ȡ +mediaPlayer.Ctrl.PublishAudio=Ƶ +mediaPlayer.Ctrl.UnPublishAudio=ȡ + + + +MultiChannel.Ctrl.ChannelList=Ƶб + + +PerCallTest.Ctrl.AudioInput=Ƶ +PerCallTest.Ctrl.AudioOutput=Ƶ +PerCallTest.Ctrl.AudioVol= +PerCallTest.Ctrl.Camera=ͷ +PerCallTest.Ctrl.StartTest=ʼ +PerCallTest.Ctrl.StopTest=ֹͣ + + +AudioVolume.Ctrl.AudioCapVol=Ƶ¼ +AudioVolume.Ctrl.AudioCapSigVol=Ƶ¼ź +AudioVolume.Ctrl.AudioPlaybackVol=Ƶ +AudioVolume.Ctrl.AudioPlaybackSigVol=Ƶź + + + +ReportInCall.Ctrl.LocalFPS = ֡ +ReportInCall.Ctrl.LocaLResoultion=ֱ +ReportInCall.Ctrl.AudioBitrate= +ReportInCall.Ctrl.AudioBytes=ֽ +ReportInCall.Ctrl.AudioNetWorkDelay=ʱ +ReportInCall.Ctrl.GopRemoteAudio=ԶƵ +ReportInCall.Ctrl.GopRemoteVideo=ԶƵ +ReportInCall.Ctrl.GopTotal=ͳ +ReportInCall.Ctrl.TotalBitrate= +ReportInCall.Ctrl.TotalBytes=ֽ +ReportInCall.Ctrl.TotalUpDownLink=д +ReportInCall.Ctrl.VideoNetWorkDelay=ʱ +ReportInCall.Ctrl.VideoBitrate= +ReportInCall.Ctrl.VideoBytes=ֽ + + +RegionConn.Ctrl.AreaCode= + + + +CrossChannel.Ctrl.CrossChannel = ԽƵ +CrossChannel.Ctrl.Token = +CrossChannel.Ctrl.Uid = ûid +CrossChannel.Ctrl.CrossChannelList = Ƶб +CrossChannel.Ctrl.AddChannel = Ƶ +CrossChannel.Ctrl.RemoveChannel = ƳƵ +CrossChannel.Ctrl.StartMediaRelay = ʼý +CrossChannel.Ctrl.StopMediaRelay = Ͽý +CrossChannel.Ctrl.UpdateMediaRelay = ý diff --git a/windows/APIExample/ci.py b/windows/APIExample/ci.py new file mode 100644 index 000000000..e48cce24b --- /dev/null +++ b/windows/APIExample/ci.py @@ -0,0 +1,21 @@ +# -*- coding: UTF-8 -*- +import re +import os + +def main(): + + appId = r'""' + if "AGORA_APP_ID" in os.environ: + appId = os.environ["AGORA_APP_ID"] + appString = "\""+appId+"\"" + else: + appString = str(appId) + f = open("./APIExample/stdafx.h", 'r+') + content = f.read() + contentNew = re.sub(r'""', appString, content) + f.seek(0) + f.write(contentNew) + f.truncate() + +if __name__ == "__main__": + main() diff --git a/windows/APIExample/install.ps1 b/windows/APIExample/install.ps1 new file mode 100644 index 000000000..3d7389616 --- /dev/null +++ b/windows/APIExample/install.ps1 @@ -0,0 +1,42 @@ +$ThirdPartysrc = 'https://agora-adc-artifacts.oss-cn-beijing.aliyuncs.com/libs/ThirdParty.zip' +$ThirdPartydes = 'ThirdParty.zip' +$agora_sdk = 'https://download.agora.io/sdk/release/Agora_Native_SDK_for_Windows_v3_2_1_FULL.zip' +$agora_des = 'Agora_Native_SDK_for_Windows_v3_2_0_FULL.zip' +$MediaPlayerSDK = 'https://download.agora.io/sdk/release/Agora_Media_Player_for_Windows_x86_32597_20200923_2306.zip' +$MediaPlayerDes = 'MediaPlayerPartSave.zip' + + +if (-not (Test-Path ThirdParty)){ + echo "download $ThirdPartydes" + mkdir ThirdParty + (New-Object System.Net.WebClient).DownloadFile($ThirdPartySrc,$ThirdPartyDes) + Unblock-File $ThirdPartyDes + Expand-Archive -Path $ThirdPartyDes -DestinationPath 'ThirdParty' -Force + Remove-Item $ThirdPartyDes -Recurse +} + + +if (-not (Test-Path libs)){ + echo "download $agora_des" + mkdir libs + (New-Object System.Net.WebClient).DownloadFile($agora_sdk,$agora_des) + Unblock-File $agora_des + Expand-Archive -Path $agora_des -DestinationPath . -Force + Move-Item Agora_Native_SDK_for_Windows_FULL\libs\* libs + Remove-Item $agora_des -Recurse + Remove-Item Agora_Native_SDK_for_Windows_FULL -Recurse +} + +if (-not (Test-Path MediaPlayerPart)){ + echo "download $MediaPlayerSDK" + mkdir MediaPlayerPart + (New-Object System.Net.WebClient).DownloadFile($MediaPlayerSDK,$MediaPlayerDes) + Unblock-File $MediaPlayerDes + Expand-Archive -Path $MediaPlayerDes -DestinationPath . -Force + Move-Item Agora_Media_Player_for_Windows_x86_tongjiangyong_32597_20200923_2306\sdk\* MediaPlayerPart + Remove-Item $MediaPlayerDes -Recurse + Remove-Item Agora_Media_Player_for_Windows_x86_tongjiangyong_32597_20200923_2306 -Recurse +} + + + diff --git a/windows/APIExample/installThirdParty.bat b/windows/APIExample/installThirdParty.bat new file mode 100644 index 000000000..b64d1fc4a --- /dev/null +++ b/windows/APIExample/installThirdParty.bat @@ -0,0 +1,4 @@ +cd /d %~dp0 + +powershell.exe -command ^ + "& {set-executionpolicy Remotesigned -Scope Process; ./'install.ps1'}" diff --git a/windows/APIExample/test/install.ps1 b/windows/APIExample/test/install.ps1 new file mode 100644 index 000000000..3d7389616 --- /dev/null +++ b/windows/APIExample/test/install.ps1 @@ -0,0 +1,42 @@ +$ThirdPartysrc = 'https://agora-adc-artifacts.oss-cn-beijing.aliyuncs.com/libs/ThirdParty.zip' +$ThirdPartydes = 'ThirdParty.zip' +$agora_sdk = 'https://download.agora.io/sdk/release/Agora_Native_SDK_for_Windows_v3_2_1_FULL.zip' +$agora_des = 'Agora_Native_SDK_for_Windows_v3_2_0_FULL.zip' +$MediaPlayerSDK = 'https://download.agora.io/sdk/release/Agora_Media_Player_for_Windows_x86_32597_20200923_2306.zip' +$MediaPlayerDes = 'MediaPlayerPartSave.zip' + + +if (-not (Test-Path ThirdParty)){ + echo "download $ThirdPartydes" + mkdir ThirdParty + (New-Object System.Net.WebClient).DownloadFile($ThirdPartySrc,$ThirdPartyDes) + Unblock-File $ThirdPartyDes + Expand-Archive -Path $ThirdPartyDes -DestinationPath 'ThirdParty' -Force + Remove-Item $ThirdPartyDes -Recurse +} + + +if (-not (Test-Path libs)){ + echo "download $agora_des" + mkdir libs + (New-Object System.Net.WebClient).DownloadFile($agora_sdk,$agora_des) + Unblock-File $agora_des + Expand-Archive -Path $agora_des -DestinationPath . -Force + Move-Item Agora_Native_SDK_for_Windows_FULL\libs\* libs + Remove-Item $agora_des -Recurse + Remove-Item Agora_Native_SDK_for_Windows_FULL -Recurse +} + +if (-not (Test-Path MediaPlayerPart)){ + echo "download $MediaPlayerSDK" + mkdir MediaPlayerPart + (New-Object System.Net.WebClient).DownloadFile($MediaPlayerSDK,$MediaPlayerDes) + Unblock-File $MediaPlayerDes + Expand-Archive -Path $MediaPlayerDes -DestinationPath . -Force + Move-Item Agora_Media_Player_for_Windows_x86_tongjiangyong_32597_20200923_2306\sdk\* MediaPlayerPart + Remove-Item $MediaPlayerDes -Recurse + Remove-Item Agora_Media_Player_for_Windows_x86_tongjiangyong_32597_20200923_2306 -Recurse +} + + + diff --git a/windows/APIExample/test/installThirdParty.bat b/windows/APIExample/test/installThirdParty.bat new file mode 100644 index 000000000..b64d1fc4a --- /dev/null +++ b/windows/APIExample/test/installThirdParty.bat @@ -0,0 +1,4 @@ +cd /d %~dp0 + +powershell.exe -command ^ + "& {set-executionpolicy Remotesigned -Scope Process; ./'install.ps1'}" diff --git a/windows/README.md b/windows/README.md new file mode 100644 index 000000000..96ff7e117 --- /dev/null +++ b/windows/README.md @@ -0,0 +1,196 @@ +# API Example Windows + +*其他语言版本: [简体中文](README.zh.md)* + +The API Example Windows Sample App is an open-source demo that show common API usage. + +This demo is written in **C++** + +## Developer Environment Requirements +* VS 2013(or higher), default is vs2017 +* Windows 7(or higher) + +### Obtain an App ID + +To build and run the sample application, get an App ID: +1. Create a developer account at [agora.io](https://dashboard.agora.io/signin/). Once you finish the signup process, you will be redirected to the Dashboard. +2. Navigate in the Dashboard tree on the left to **Projects** > **Project List**. +3. Save the **App ID** from the Dashboard for later use. +4. Generate a temp **Access Token** (valid for 24 hours) from dashboard page with given channel name, save for later use. +5. Define the APP_ID with your App ID. + + ``` + #define APP_ID _T("Your App ID") + ``` +6. (Optional)Alternate approach to setup your APPID is to create an AppId.ini file under Debug/Release. Modify the appId value to the App ID you just applied. + + ``` + #[AppID] + + #AppID=xxxxxxxxxxxxxxxxxxx + ``` + +### Build the application +**This open source sample project uses the Agora RTC SDK,DirectShow SDK, and MeidaPlayer SDK.** + +You can directly run `APIExample/installThirdParty.bat` to automatically environment configuration.Once the configuration is complete, open the project with VS2017, select the x86 version to compile and run. + +## Basic Scene + + +### LiveBroadcasting + + +* change client role +* support 1v1,1v3, 1v8, 1v15 +* join/leave channel +* render local and remote video + + +## Advanced Scene + + +### Zone access restrictions + +* Specify SDK access restrictions through Area Code + +### Cross-channel media streaming + +* Send the anchor flow of Channel A to Channel B to achieve anchor PK + +### Add multiple channels + +* Use joinChannel to join channels +* Multiple other channels can be joined using RtcChannel + +### Quality monitoring during calls + +* Upstream and downstream network quality +* Statistical Information +* Audio and video quality + +### Adjust the call volume + +* Collection volume for local users +* Local playback volume for remote users +* Ear-Return volume +* User volume prompt + +### Pre-call device and network detection + +* Echo test +* Audio acquisition equipment test +* Audio playback device test +* Joint testing of audio acquisition and playback equipment +* Test of video acquisition equipment + + +### RTMP Streaming + +* Add publish stream url after join channel success +* remove publish stream url before leave channel +* show information returned by rtmp streaming callback + +### Inject Stream Url + +* inject stream url after join channel success +* show information returned by inject status callback +* Receive 666 joined callback after inject stream url succeed.You can mute video and audio of 666. Also,you can render it. +* remove inject stream url before leave channel + +### Video Metadata(Video SEI) + +* You need enable video and joinchannel. +* Send video SEI information. The maximum is 1024 byte. +* Receive SEI information and show it. +* Clear SEI information + +### Share the screen + +* Enter the channel and enumerate all visible Windows +* Select a visible window +* Recording screen +* Stop recording + +### Beauty + +* Set lighteningContrastLevel +* Set lighteningLevel +* Set rednessLevel +* Set smoothnessLevel + +### Beauty Audio + +* Set up sound effects or audio beauty + +### Audio Profile + +* Set profile +* Set scenario +* Set audio property to channel audio + +### Audio Mixing + +* Set the audio path +* Set the number of playback times +* Sets whether to play locally only +* Sets whether to replace the microphone audio + +### Camera Capture And Render +There are two ways for SDK to realize self-collection. One is to use pushVideoFrame to actively push video frames to SDK without local rendering. In the demo, DirectX is used for local rendering; the other is to use MediaIO, which is used by SDK for local rendering of images. + +* Camera capture using DirectShow +* Enumerates all image acquisition devices and types +* Create image acquisition filters +* Start collecting camera data +* SDK acquires camera data +* Stop collecting camera data + +### Process Raw Video Data + +* Sign up as a video observer +* Process video frames in onCaptureVideoFrame + +### Audio Capture And Render +Custom audio capture use MeidaIO method for capture, use Sink method to obtain audio data, and then use DirectSound for local rendering. + +* Audio acquisition using DirectShow +* Enumerates all audio acquisition devices and types +* Create audio capture filters +* Start collecting microphone data +* SDK gets microphone data +* Stop collecting microphone data + +### Process Raw Audio Data + +* Register Audio Observer +* Process Audio Frames in onRecordAudioFrame + +### Custom Encrypt + +* Register Packet Observer +* Encrypt the audio stream before sending it in onSendAudioPacket +* Encrypt the video stream before it is sent in the onSendVideoPacket +* Decrypt the audio stream after receiving it in onReceiveAudioPacket +* Decrypt a video stream after receiving it from an onReceiveVideoPacket + +### Meida Player Kit + +* Use MediaPlayer Kit for media opening, playing and other operations. +* Use the MediaPlayerExtensions to push the flow to the AgoraRtc Engine's channels. +* Use the IMediaPlayerObserver to handle MeidaPlayer callback events.For example (open stream, play stream) + + +## Connect Us + +- For potential issues, take a look at our [FAQ](https://docs.agora.io/cn/faq) first +- Dive into [Agora SDK Samples](https://github.com/AgoraIO) to see more tutorials +- Take a look at [Agora Use Case](https://github.com/AgoraIO-usecase) for more complicated real use case +- Repositories managed by developer communities can be found at [Agora Community](https://github.com/AgoraIO-Community) +- You can find full API documentation at [Document Center](https://docs.agora.io/en/) +- If you encounter problems during integration, you can ask question in [Stack Overflow](https://stackoverflow.com/questions/tagged/agora.io) +- You can file bugs about this sample at [issue](https://github.com/AgoraIO/Basic-Video-Broadcasting/issues) + +## License + +The MIT License (MIT). diff --git a/windows/README.zh.md b/windows/README.zh.md new file mode 100644 index 000000000..923db53de --- /dev/null +++ b/windows/README.zh.md @@ -0,0 +1,207 @@ +# API Example Windows + +*Read this in other languages: [English](README.md)* + +这个开源示例项目演示了如何快速集成Agora视频SDK,展示了常用场景的API示例 + +本开源项目使用 **C++** 语言 + +## 环境主备 +* vs 2013(或更高版本),默认支持vs2017。 +* Windows 7(或更高版本)。 + +**注意** 使用其他版本需要自行修改该配置,编译release还需要安装兼容xp相关插件。 + + +## 运行示例程序 + +这个段落主要讲解了如何编译和运行实例程序。 + +### 创建Agora账号并获取AppId + +在编译和启动实例程序前,您需要首先获取一个可用的App ID: +1. 在[agora.io](https://dashboard.agora.io/signin/)创建一个开发者账号 +2. 前往后台页面,点击左部导航栏的 **项目 > 项目列表** 菜单 +3. 复制后台的 **App ID** 并备注,稍后启动应用时会用到它 +4. 在项目页面生成临时 **Access Token** (24小时内有效)并备注,注意生成的Token只能适用于对应的频道名。 + +5. 将 AppID 内容替换至 APP_ID 宏定义中 + + ``` + #define APP_ID _T("Your App ID") + ``` +6. (可选)你也可以在Debug/Release目录下创建一个AppId.ini文件以配置你应用程序的AppID, 修改AppId的值为刚才申请的App ID + ``` + [AppID] + AppID=xxxxxxxxxxxxxxxxxxx + ``` + +### 编译项目 +**这个开源示例项目使用了Agora RTC SDK,DirectShow SDK,MeidaPlayer SDK。** + +你可以通过直接运行`APIExample/installThirdParty.bat`来自动进行依赖下载与环境配置。配置完成后使用vs2017打开项目,选择x86版本进行编译就可以运行了。 + +## 基础场景 + +### 直播互动 + +* 切换角色 +* 支持1v1,1v3, 1v8, 1v15 +* 进出频道 +* 显示本地和远端视频 + +## 进阶场景 + +### 区域访问限制 +* 通过 area code 指定 SDK 访问限制 + + +### 跨频道媒体流转发 +* 将 A 频道的主播流转发到 B 频道,实现主播 PK + + +### 加入多频道 +* 使用joinChannel加入频道 +* 可以使用RtcChannel加入多个其他频道 + + +### 通话中质量监测 +* 上下行网络质量 +* 统计信息 +* 音视频质量 + +### 调整通话音量 + +* 本地用户的采集音量 +* 远端用户在本地的播放音量 +* 耳返音量 +* 用户音量提示 + +### 通话前设备和网络检测 + +* 回声测试 +* 音频采集设备测试 +* 音频播放设备测试 +* 音频采集和播放设备联合测试 +* 视频采集设备测试 + +### 旁路推流 + +* 加入频道后添加rtmp推流地址 +* 移除推流地址 +* 推流回调处理状态信息显示 + +### 插入媒体流 + +* 加入频道后inject 媒体流 +* 显示插入状态回调信息 +* 插入成功,收到一个666加入回调。本地mute 666的视频和音频(也可以不mute) +* 移除插入流 + +### 视频MetaData + +* 加入频道,发送视频流 +* 发送视频SEI信息,最大1024B +* 接收视频SEI信息 +* 清除SEI信息 + + +### 共享屏幕 + +* 进入频道,枚举所有可见窗口 +* 选择一个可见窗口 +* 录制屏幕 +* 停止录制 + +### 美颜 + +* 设置明暗对比等级 +* 设置明亮度 +* 设置红润度 +* 设置平滑度 + +### 美声 + +* 设置音效或者美声 + +### 音频设置 + +* 设置音频参数 +* 设置场景 +* 设置频道内的音频设置 + +### 音频混合 + +* 设置音频路径 +* 设置播放次数 +* 设置是否仅仅本地播放 +* 设置是否替换麦克风音频 + +### 播放音效文件 +* 设置音频路径 +* 播放音效 +* 可以暂停和恢复指定的音效 +* 可以暂停和恢复所有音效 + + +### 自定义摄像头采集和渲染 +sdk实现自采集的方式有2种,一种是使用pushVideoFrame主动向sdk推送视频帧,sdk不会本地渲染,demo中使用DirectX进行本地渲染,一种是使用MediaIO的方式,MediaIO的方式sdk会对图像进行本地渲染。 + +* 摄像头采集使用DirectShow +* 枚举所有图像采集设备和类型 +* 创建图像采集过滤器 +* 开始采集摄像头数据 +* SDK获取摄像头数据 +* 停止采集摄像头数据 + + +### 处理视频原始数据 + +* 注册视频观察者 +* 实现了对原始图像进行灰度处理,和模糊处理 +* 在onCaptureVideoFrame中对视频帧进行处理 + + +### 自定义音频采集和渲染 +自定义音频采集使用MeidaIO的方式进行采集,使用Sink的方式获得音频数据,之后使用DirectSound进行本地渲染。 +* 音频采集使用DirectShow +* 枚举所有音频采集设备和类型 +* 创建音频采集过滤器 +* 开始采集麦克风数据 +* SDK获取麦克风数据 +* 停止采集麦克风数据 + + +### 处理音频原始数据 + +* 注册音频观察者 +* 在onRecordAudioFrame中对音频帧进行处理 + + +### 自定义媒体加密 + +* 注册数据包观察者 +* 在onSendAudioPacket中对音频流发送前进行加密 +* 在onSendVideoPacket中对视频流发送前进行加密 +* 在onReceiveAudioPacket中对音频流接收后进行解密 +* 在onReceiveVideoPacket中对视频流接收后进行解密 + +### 媒体播放器组件 + +* 使用MeidaPlayer Kit 进行媒体的打开,播放等操作。 +* 使用MeidaPlayerExtensions 向AgoraRtc Engine的频道推流。 +* 使用IMediaPlayerObserver来处理MeidaPlayer的回调事件。例如(打开,播放) + +## 联系我们 + +- 如果你遇到了困难,可以先参阅[常见问题](https://docs.agora.io/cn/faq) +- 如果你想了解更多官方示例,可以参考[官方SDK示例](https://github.com/AgoraIO) +- 如果你想了解声网SDK在复杂场景下的应用,可以参考[官方场景案例](https://github.com/AgoraIO-usecase) +- 如果你想了解声网的一些社区开发者维护的项目,可以查看[社区](https://github.com/AgoraIO-Community) +- 完整的 API 文档见 [文档中心](https://docs.agora.io/cn/) +- 若遇到问题需要开发者帮助,你可以到 [开发者社区](https://rtcdeveloper.com/) 提问 +- 如果发现了示例代码的 bug,欢迎提交 [issue](https://github.com/AgoraIO/Basic-Video-Broadcasting/issues) + +## 代码许可 + +The MIT License (MIT). diff --git a/windows/cicd/script/downloadsdk1.ps1 b/windows/cicd/script/downloadsdk1.ps1 new file mode 100644 index 000000000..44ecd029a --- /dev/null +++ b/windows/cicd/script/downloadsdk1.ps1 @@ -0,0 +1,18 @@ +param($SDKURL, $LocalFileName) + +add-type @" +using System.Net; +using System.Security.Cryptography.X509Certificates; +public class TrustAllCertsPolicy : ICertificatePolicy { + public bool CheckValidationResult( + ServicePoint srvPoint, X509Certificate certificate, + WebRequest request, int certificateProblem) { + return true; + } +} +"@ +$AllProtocols = [System.Net.SecurityProtocolType]'Ssl3,Tls,Tls11,Tls12' +[System.Net.ServicePointManager]::SecurityProtocol = $AllProtocols +[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy + +Invoke-WebRequest -Uri "$SDKURL" -OutFile "$LocalFileName" -TimeoutSec 10; diff --git a/windows/cicd/templates/build-Windows.yml b/windows/cicd/templates/build-Windows.yml new file mode 100644 index 000000000..7ff5ab297 --- /dev/null +++ b/windows/cicd/templates/build-Windows.yml @@ -0,0 +1,115 @@ +# .NET Desktop +# Build and run tests for .NET Desktop or Windows classic desktop solutions. +# Add steps that publish symbols, save build artifacts, and more: +# https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net + +parameters: + displayName: '' + workingDirectory: '' + scheme: '' + sdkVersion: '' + faceUnitySDKVersion: '' + Machine: '' + solutionName: '' + release: '' +jobs: + - job: ${{ parameters.displayName }}Build + displayName: ${{ parameters.displayName }} + + pool: + vmImage: 'vs2017-win2016' + + variables: + - group: AgoraKeys + - name: solution + value: '${{ parameters.workingDirectory }}/${{ parameters.solutionName }}' + - name: buildPlatform + value: '${{ parameters.Machine }}' + - name: buildConfiguration + value: 'Release' + - name: WindowsRTCSDK + value: 'https://download.agora.io/sdk/release/Agora_Native_SDK_for_Windows_v3_2_0_FULL.zip' + - name: Windows-ThirdParty + value: 'https://github.com/AgoraIO/Advanced-Video/releases/download/Dshow/ThirdParty.zip' + - name: sdkunzipPath + value: 'AgoraSDK/Agora_Native_SDK_for_Windows_FULL' + - name: MediaPlayer + value: 'https://download.agora.io/sdk/release/Agora_Media_Player_for_Windows_x86_rel.v1.1.0.16486_20200507_1537.zip' + steps: + - script: echo "$(WindowsRTCSDK)" && ls + #AgoraSDK + - powershell: Windows/cicd/script/downloadsdk1.ps1 "$(WindowsRTCSDK)" "AgoraSDK.zip" + #ThirdParty + - powershell: Windows/cicd/script/downloadsdk1.ps1 "$(Windows-ThirdParty)" "ThirdParty.zip" + #MediaPlayer + - powershell: Windows/cicd/script/downloadsdk1.ps1 "$(MediaPlayer)" "MediaPlayer.zip" + - script: cd ${{ parameters.workingDirectory }} && ls && python ci.py + env: + AGORA_APP_ID: $(agora.appId) + + - task: ExtractFiles@1 + inputs: + archiveFilePatterns: 'AgoraSDK.zip' + destinationFolder: 'AgoraSDK' + + - task: ExtractFiles@1 + inputs: + archiveFilePatterns: 'ThirdParty.zip' + destinationFolder: 'ThirdParty' + + - task: ExtractFiles@1 + inputs: + archiveFilePatterns: 'MediaPlayer.zip' + destinationFolder: 'MediaPlayer' + + - task: CopyFiles@2 + inputs: + sourceFolder: 'ThirdParty/DShow' + targetFolder: ${{ parameters.workingDirectory }}/ThirdParty/DShow + + - task: CopyFiles@2 + inputs: + sourceFolder: 'ThirdParty/libyuv' + targetFolder: ${{ parameters.workingDirectory }}/ThirdParty/libyuv + + - task: CopyFiles@2 + inputs: + sourceFolder: 'MediaPlayer/Agora_Media_Player_for_Windows_x86_rel.v1.1.0.16486_20200507_1537/sdk/include' + targetFolder: ${{ parameters.workingDirectory }}/MediaPlayerPart/include + - task: CopyFiles@2 + inputs: + sourceFolder: 'MediaPlayer/Agora_Media_Player_for_Windows_x86_rel.v1.1.0.16486_20200507_1537/sdk/lib' + targetFolder: ${{ parameters.workingDirectory }}/MediaPlayerPart/lib + - task: CopyFiles@2 + inputs: + sourceFolder: 'MediaPlayer/Agora_Media_Player_for_Windows_x86_rel.v1.1.0.16486_20200507_1537/sdk/dll' + targetFolder: ${{ parameters.workingDirectory }}/MediaPlayerPart/dll + + - script: ls + + - task: CopyFiles@2 + inputs: + sourceFolder: $(sdkunzipPath)/libs + targetFolder: ${{ parameters.workingDirectory }}/libs + + - task: VSBuild@1 + inputs: + solution: '$(solution)' + platform: '$(buildPlatform)' + configuration: '$(buildConfiguration)' + vsVersion: '15.0' + msbuildArchitecture: '${{ parameters.Machine }}' + - script: cd ${{ parameters.workingDirectory }}/${{ parameters.release }} && ls + - task: ArchiveFiles@2 + inputs: + rootFolderOrFile: ${{ parameters.workingDirectory }}/${{ parameters.release }} + includeRootFolder: true + archiveType: 'zip' + archiveFile: ${{ parameters.workingDirectory }}/${{ parameters.displayName }}.zip + replaceExistingArchive: true + + - task: PublishBuildArtifacts@1 + inputs: + PathtoPublish: ${{ parameters.workingDirectory }}/${{ parameters.displayName }}.zip + ArtifactName: ${{ parameters.displayName }} +