├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── release │ └── output.json └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── github │ │ └── devjn │ │ └── webrtcandroidfirebase │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── devjn │ │ │ └── webrtcandroidfirebase │ │ │ ├── FirebaseData.kt │ │ │ ├── IntroActivity.kt │ │ │ ├── LobbyActivity.kt │ │ │ ├── VideoCallActivity.kt │ │ │ ├── components │ │ │ ├── GLCircleDrawer.java │ │ │ └── RoundedView.kt │ │ │ └── videocall │ │ │ ├── AsyncSDPHelpers.kt │ │ │ ├── FirebaseSignaler.kt │ │ │ └── VideoCallSession.kt │ └── res │ │ ├── drawable-hdpi │ │ ├── ic_action_camera_switch.png │ │ └── ic_call_end_white_48dp.png │ │ ├── drawable-mdpi │ │ ├── ic_action_camera_switch.png │ │ └── ic_call_end_white_48dp.png │ │ ├── drawable-nodpi │ │ └── webrtc_logo.png │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-xhdpi │ │ ├── ic_action_camera_switch.png │ │ └── ic_call_end_white_48dp.png │ │ ├── drawable-xxhdpi │ │ ├── ic_action_camera_switch.png │ │ └── ic_call_end_white_48dp.png │ │ ├── drawable-xxxhdpi │ │ └── ic_call_end_white_48dp.png │ │ ├── drawable │ │ ├── ic_call_end.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_switch_camera.xml │ │ ├── round_green.xml │ │ ├── round_red.xml │ │ └── rounded_corner.xml │ │ ├── layout │ │ ├── activity_container.xml │ │ ├── activity_intro.xml │ │ ├── activity_lobby.xml │ │ ├── activity_video_call.xml │ │ └── list_item_contact.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── github │ └── devjn │ └── webrtcandroidfirebase │ └── ExampleUnitTest.kt ├── build.gradle ├── files ├── call_screen.png ├── contact_screen.png └── webrt-test-app.apk ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | app/release/ 11 | app/google-services.json 12 | .idea/ 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebRTCAndroidFirebase 2 | Native android video chat application implemented using WebRTC and Firebase Database as signaler. 3 | 4 | This project intention is to demonstrate how WebRTC can be implemented as native app and show it's capabilites. 5 | 6 | ![Call screen](/files/call_screen.png?raw=true ) 7 | ![Contacts list](/files/contact_screen.png?raw=true) 8 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 27 7 | defaultConfig { 8 | applicationId "com.github.devjn.webrtcandroidfirebase" 9 | minSdkVersion 17 10 | targetSdkVersion 27 11 | versionCode 1 12 | versionName "1.0" 13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 14 | vectorDrawables.useSupportLibrary = true 15 | } 16 | buildTypes { 17 | debug { 18 | debuggable true 19 | jniDebuggable true 20 | 21 | minifyEnabled false 22 | } 23 | 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | 34 | kotlinOptions { 35 | jvmTarget = "1.8" 36 | } 37 | } 38 | 39 | } 40 | 41 | dependencies { 42 | implementation fileTree(dir: 'libs', include: ['*.jar']) 43 | implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 44 | implementation 'org.webrtc:google-webrtc:1.0.22512' 45 | implementation 'com.squareup.okhttp3:okhttp:3.10.0' 46 | 47 | implementation "com.android.support:design:$support_v" 48 | implementation "com.android.support:cardview-v7:$support_v" 49 | implementation "com.android.support:support-v4:$support_v" 50 | implementation "com.android.support:appcompat-v7:$support_v" 51 | 52 | implementation "com.google.firebase:firebase-core:$play_version" 53 | implementation "com.google.firebase:firebase-auth:$play_version" 54 | implementation "com.google.firebase:firebase-database:$play_version" 55 | implementation 'com.firebaseui:firebase-ui-auth:3.2.2' 56 | 57 | implementation 'com.android.support.constraint:constraint-layout:1.0.2' 58 | 59 | implementation "com.jn.arts.jnlibs:ui-utils:1.1.0" 60 | 61 | 62 | testImplementation 'junit:junit:4.12' 63 | androidTestImplementation 'com.android.support.test:runner:1.0.1' 64 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' 65 | } 66 | 67 | apply plugin: 'com.google.gms.google-services' -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/release/output.json: -------------------------------------------------------------------------------- 1 | [{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":1},"path":"app-release.apk","properties":{"packageId":"com.github.devjn.webrtcandroidfirebase","split":"","minSdkVersion":"17"}}] -------------------------------------------------------------------------------- /app/src/androidTest/java/com/github/devjn/webrtcandroidfirebase/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.devjn.webrtcandroidfirebase 2 | 3 | import android.support.test.InstrumentationRegistry 4 | import android.support.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("com.github.devjn.webrtcandroidfirebase", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/devjn/webrtcandroidfirebase/FirebaseData.kt: -------------------------------------------------------------------------------- 1 | package com.github.devjn.webrtcandroidfirebase 2 | 3 | import com.google.firebase.auth.FirebaseAuth 4 | import com.google.firebase.database.FirebaseDatabase 5 | 6 | 7 | /** 8 | * Created by @author Jahongir on 13-Feb-18 9 | * devjn@jn-arts.com 10 | * FirebaseData 11 | */ 12 | object FirebaseData { 13 | 14 | var myID: String = "" 15 | const val CALLS = "calls" 16 | 17 | val database = FirebaseDatabase.getInstance() 18 | 19 | 20 | fun getCallDataPath(id: String) = "${CALLS}/$id/data" 21 | fun getCallStatusPath(id: String) = "${CALLS}/$id/status" 22 | 23 | fun getCallDataReference(id: String) = database.getReference("${CALLS}/$id/data")!! 24 | fun getCallStatusReference(id: String) = database.getReference("${CALLS}/$id/status")!! 25 | fun getCallIdReference(id: String) = database.getReference("${CALLS}/$id/id")!! 26 | 27 | fun init() { 28 | val auth = FirebaseAuth.getInstance()!! 29 | auth.currentUser?.let { 30 | myID = it.uid 31 | database.getReference("users/${myID}/online").onDisconnect().setValue(false) 32 | database.getReference("users/${myID}").setValue(ContactData(it.displayName, true)) 33 | } 34 | 35 | } 36 | 37 | } 38 | 39 | data class ContactData(val name: String? = "", val online: Boolean = false) -------------------------------------------------------------------------------- /app/src/main/java/com/github/devjn/webrtcandroidfirebase/IntroActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.devjn.webrtcandroidfirebase 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.support.design.widget.Snackbar 7 | import android.support.v7.app.AppCompatActivity 8 | import android.widget.Button 9 | import com.firebase.ui.auth.AuthUI 10 | import com.firebase.ui.auth.ErrorCodes 11 | import com.firebase.ui.auth.IdpResponse 12 | import com.google.firebase.auth.FirebaseAuth 13 | import java.util.* 14 | 15 | 16 | class IntroActivity : AppCompatActivity() { 17 | 18 | private val RC_SIGN_IN = 123 19 | 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | setContentView(R.layout.activity_intro) 23 | 24 | val auth = FirebaseAuth.getInstance() 25 | if (auth.currentUser != null) { 26 | // already signed in 27 | startActivity(Intent(this, LobbyActivity::class.java)) 28 | finish() 29 | return 30 | } 31 | 32 | val startButton: Button = findViewById(R.id.btn_start) 33 | startButton.setOnClickListener { 34 | startActivityForResult( 35 | AuthUI.getInstance() 36 | .createSignInIntentBuilder() 37 | .setLogo(R.drawable.webrtc_logo) 38 | .setIsSmartLockEnabled(!BuildConfig.DEBUG /* credentials */, true /* hints */) 39 | .setAvailableProviders(Arrays.asList( 40 | AuthUI.IdpConfig.EmailBuilder().build(), 41 | AuthUI.IdpConfig.GoogleBuilder().build() 42 | )).build(), 43 | RC_SIGN_IN) 44 | } 45 | 46 | } 47 | 48 | 49 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) { 50 | super.onActivityResult(requestCode, resultCode, data) 51 | // RC_SIGN_IN is the request code you passed into startActivityForResult(...) when starting the sign in flow. 52 | if (requestCode == RC_SIGN_IN) { 53 | val response = IdpResponse.fromResultIntent(data) 54 | 55 | // Successfully signed in 56 | if (resultCode == Activity.RESULT_OK) { 57 | startActivity(Intent(this, LobbyActivity::class.java)) 58 | finish() 59 | return 60 | } else { 61 | // Sign in failed 62 | if (response == null) { 63 | // User pressed back button 64 | showSnackbar(R.string.sign_in_cancelled) 65 | return 66 | } 67 | 68 | if (response.errorCode == ErrorCodes.NO_NETWORK) { 69 | showSnackbar(R.string.no_internet_connection) 70 | return 71 | } 72 | 73 | if (response.errorCode == ErrorCodes.UNKNOWN_ERROR) { 74 | showSnackbar(R.string.unknown_error) 75 | return 76 | } 77 | } 78 | 79 | showSnackbar(R.string.unknown_sign_in_response) 80 | } 81 | } 82 | 83 | private fun showSnackbar(stringRes: Int) { 84 | Snackbar.make(findViewById(R.id.root)!!, stringRes, Snackbar.LENGTH_LONG).show() 85 | } 86 | 87 | 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/devjn/webrtcandroidfirebase/LobbyActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.devjn.webrtcandroidfirebase 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.os.Bundle 6 | import android.support.v7.app.AppCompatActivity 7 | import android.util.Log 8 | import android.view.LayoutInflater 9 | import android.view.View 10 | import android.view.ViewGroup 11 | import android.widget.* 12 | import com.github.devjn.webrtcandroidfirebase.FirebaseData.myID 13 | import com.google.firebase.database.DataSnapshot 14 | import com.google.firebase.database.DatabaseError 15 | import com.google.firebase.database.DatabaseReference 16 | import com.google.firebase.database.ValueEventListener 17 | 18 | 19 | class LobbyActivity : AppCompatActivity() { 20 | 21 | private lateinit var adapter: ArrayAdapter> 22 | 23 | private lateinit var callRef: DatabaseReference 24 | 25 | @Suppress("UNUSED_ANONYMOUS_PARAMETER") 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | setContentView(R.layout.activity_lobby) 29 | 30 | val listView: ListView = findViewById(R.id.list) 31 | adapter = ContactsAdapter(this, ArrayList>(0)) 32 | adapter.setNotifyOnChange(true) 33 | listView.adapter = adapter 34 | 35 | listView.onItemClickListener = AdapterView.OnItemClickListener { parent, view, position, id -> 36 | Log.w("TAG", "clicked: " + adapter.getItem(position)) 37 | startVideoCall(adapter.getItem(position).first) 38 | } 39 | val emptyTextView = TextView(this) 40 | emptyTextView.setText("No contacts available") 41 | listView.emptyView = emptyTextView 42 | 43 | init() 44 | } 45 | 46 | @SuppressLint("SetTextI18n") 47 | private fun init() { 48 | FirebaseData.init() 49 | 50 | callRef = FirebaseData.database.getReference("calls/$myID/id") 51 | 52 | val textView: TextView = findViewById(R.id.textView) 53 | textView.text = "My id: $myID" 54 | } 55 | 56 | 57 | override fun onResume() { 58 | super.onResume() 59 | callRef.addValueEventListener(callListener) 60 | val usersRef = FirebaseData.database.getReference("users") 61 | usersRef.addValueEventListener(usersListener) 62 | } 63 | 64 | override fun onPause() { 65 | super.onPause() 66 | callRef.removeEventListener(callListener) 67 | val usersRef = FirebaseData.database.getReference("users") 68 | usersRef.addValueEventListener(usersListener) 69 | } 70 | 71 | private fun startVideoCall(key: String) { 72 | FirebaseData.getCallStatusReference(myID).setValue(true) 73 | FirebaseData.getCallIdReference(key).onDisconnect().removeValue() 74 | FirebaseData.getCallIdReference(key).setValue(myID) 75 | VideoCallActivity.startCall(this@LobbyActivity, key) 76 | } 77 | 78 | private fun receiveVideoCall(key: String) { 79 | VideoCallActivity.receiveCall(this, key) 80 | } 81 | 82 | 83 | private val callListener = object : ValueEventListener { 84 | override fun onDataChange(dataSnapshot: DataSnapshot) { 85 | if (dataSnapshot.exists()) { 86 | receiveVideoCall(dataSnapshot.getValue(String::class.java)!!) 87 | callRef.removeValue() 88 | } 89 | } 90 | 91 | override fun onCancelled(p0: DatabaseError?) { 92 | } 93 | } 94 | 95 | private val usersListener = object : ValueEventListener { 96 | override fun onDataChange(dataSnapshot: DataSnapshot) { 97 | adapter.clear() 98 | if (!dataSnapshot.exists()) { 99 | return 100 | } 101 | dataSnapshot.children.forEach { 102 | if (it.exists() && it.key != myID) 103 | adapter.add(Pair(it.key, it.getValue(ContactData::class.java)!!)) 104 | } 105 | } 106 | 107 | override fun onCancelled(p0: DatabaseError?) { 108 | } 109 | } 110 | 111 | 112 | inner class ContactsAdapter(context: Context, contacts: List>) : ArrayAdapter>(context, 0, contacts) { 113 | 114 | override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { 115 | var convertView = convertView 116 | val contact = getItem(position) 117 | if (convertView == null) { 118 | convertView = LayoutInflater.from(context).inflate(R.layout.list_item_contact, parent, false) 119 | } 120 | 121 | val txtName = convertView!!.findViewById(R.id.txtName) as TextView 122 | val imageView = convertView.findViewById(R.id.imageView) as ImageView 123 | // Populate the data into the template view using the data object 124 | txtName.text = contact.second.name 125 | imageView.setImageResource(if(contact.second.online) R.drawable.round_green else R.drawable.round_red) 126 | 127 | return convertView 128 | } 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/devjn/webrtcandroidfirebase/VideoCallActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.devjn.webrtcandroidfirebase 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import android.graphics.PixelFormat 7 | import android.media.AudioManager 8 | import android.os.Bundle 9 | import android.support.v4.app.ActivityCompat 10 | import android.support.v4.app.Fragment 11 | import android.support.v4.content.ContextCompat 12 | import android.support.v7.app.AppCompatActivity 13 | import android.util.Log 14 | import android.view.LayoutInflater 15 | import android.view.View 16 | import android.view.ViewGroup 17 | import android.widget.ImageButton 18 | import android.widget.TextView 19 | import com.github.devjn.webrtcandroidfirebase.components.GLCircleDrawer 20 | import com.github.devjn.webrtcandroidfirebase.videocall.VideoCallSession 21 | import com.github.devjn.webrtcandroidfirebase.videocall.VideoCallStatus 22 | import com.github.devjn.webrtcandroidfirebase.videocall.VideoRenderers 23 | import org.webrtc.EglBase 24 | import org.webrtc.RendererCommon 25 | import org.webrtc.SurfaceViewRenderer 26 | 27 | 28 | class VideoCallActivity : AppCompatActivity() { 29 | 30 | companion object { 31 | private const val CAMERA_AUDIO_PERMISSION_REQUEST = 1 32 | private const val TAG = "VideoCallActivity" 33 | 34 | fun startCall(context: Context, id: String) { 35 | val starter = Intent(context, VideoCallActivity::class.java) 36 | starter.putExtra("offer", true) 37 | starter.putExtra("id", id) 38 | context.startActivity(starter) 39 | } 40 | 41 | fun receiveCall(context: Context, id: String) { 42 | val starter = Intent(context, VideoCallActivity::class.java) 43 | starter.putExtra("offer", false) 44 | starter.putExtra("id", id) 45 | context.startActivity(starter) 46 | } 47 | } 48 | 49 | override fun onCreate(savedInstanceState: Bundle?) { 50 | super.onCreate(savedInstanceState) 51 | setContentView(R.layout.activity_container) 52 | 53 | if (savedInstanceState == null) { 54 | val fragment = CallFragment() 55 | fragment.isOffer = intent.getBooleanExtra("offer", false) 56 | fragment.id = intent.getStringExtra("id") 57 | 58 | supportFragmentManager.beginTransaction() 59 | .replace(R.id.container, fragment, "CallFragment") 60 | .addToBackStack(null) 61 | .commit() 62 | } 63 | } 64 | 65 | 66 | class CallFragment : Fragment() { 67 | private var videoSession: VideoCallSession? = null 68 | private lateinit var statusTextView: TextView 69 | private lateinit var localVideoView: SurfaceViewRenderer 70 | private lateinit var remoteVideoView: SurfaceViewRenderer 71 | private var audioManager: AudioManager? = null 72 | private var savedMicrophoneState: Boolean? = null 73 | private var savedAudioMode: Int? = null 74 | 75 | var isOffer = false 76 | lateinit var id: String 77 | 78 | override fun onCreate(savedInstanceState: Bundle?) { 79 | super.onCreate(savedInstanceState) 80 | retainInstance = true 81 | 82 | audioManager = context?.getSystemService(Context.AUDIO_SERVICE) as AudioManager? 83 | savedAudioMode = audioManager?.mode 84 | audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION 85 | 86 | savedMicrophoneState = audioManager?.isMicrophoneMute 87 | audioManager?.isMicrophoneMute = false 88 | audioManager?.isSpeakerphoneOn = true 89 | } 90 | 91 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { 92 | val root = inflater.inflate(R.layout.activity_video_call, container, false) 93 | 94 | statusTextView = root.findViewById(R.id.status_text) 95 | localVideoView = root.findViewById(R.id.pip_video) 96 | remoteVideoView = root.findViewById(R.id.remote_video) 97 | 98 | val hangup: ImageButton = root.findViewById(R.id.hangup_button) 99 | hangup.setOnClickListener { activity!!.finish() } 100 | 101 | val toggle: ImageButton = root.findViewById(R.id.btn_toggle_camera) 102 | toggle.setOnClickListener { videoSession?.toggleCamera() } 103 | 104 | return root 105 | } 106 | 107 | override fun onActivityCreated(savedInstanceState: Bundle?) { 108 | super.onActivityCreated(savedInstanceState) 109 | if (savedInstanceState == null) 110 | handlePermissions() 111 | else videoSession?.let { 112 | initVideoVews() 113 | it.videoRenderers.updateViewRenders(localVideoView, remoteVideoView) 114 | } 115 | } 116 | 117 | override fun onDestroyView() { 118 | super.onDestroyView() 119 | localVideoView.release() 120 | remoteVideoView.release() 121 | } 122 | 123 | override fun onDestroy() { 124 | super.onDestroy() 125 | videoSession?.terminate() 126 | 127 | if (savedAudioMode !== null) { 128 | audioManager?.mode = savedAudioMode!! 129 | } 130 | if (savedMicrophoneState != null) { 131 | audioManager?.isMicrophoneMute = savedMicrophoneState!! 132 | } 133 | } 134 | 135 | private fun onStatusChanged(newStatus: VideoCallStatus) { 136 | Log.d(TAG, "New call status: $newStatus") 137 | if (!isAdded) { 138 | Log.w(TAG, "onStatusChanged, but is not added : $newStatus") 139 | return 140 | } 141 | activity?.runOnUiThread { 142 | when (newStatus) { 143 | VideoCallStatus.FINISHED -> activity!!.finish() 144 | else -> { 145 | statusTextView.text = resources.getString(newStatus.label) 146 | statusTextView.setTextColor(ContextCompat.getColor(context!!, newStatus.color)) 147 | } 148 | } 149 | } 150 | } 151 | 152 | private fun handlePermissions() { 153 | val canAccessCamera = ContextCompat.checkSelfPermission(context!!, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED 154 | val canRecordAudio = ContextCompat.checkSelfPermission(context!!, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED 155 | if (!canAccessCamera || !canRecordAudio) { 156 | ActivityCompat.requestPermissions(activity!!, arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO), CAMERA_AUDIO_PERMISSION_REQUEST) 157 | } else { 158 | startVideoSession() 159 | } 160 | } 161 | 162 | private fun startVideoSession() { 163 | videoSession = VideoCallSession.connect(context!!, id, isOffer, VideoRenderers(localVideoView, remoteVideoView), this::onStatusChanged) 164 | initVideoVews() 165 | } 166 | 167 | private fun initVideoVews() { 168 | localVideoView.apply { 169 | init(videoSession?.renderContext, null, EglBase.CONFIG_RGBA, GLCircleDrawer()) 170 | setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) 171 | // setZOrderMediaOverlay(true) 172 | //To make transparent 173 | setZOrderOnTop(true) 174 | holder.setFormat(PixelFormat.TRANSLUCENT) 175 | setEnableHardwareScaler(true) 176 | setMirror(true) 177 | } 178 | 179 | remoteVideoView.apply { 180 | init(videoSession?.renderContext, null) 181 | setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL) 182 | setEnableHardwareScaler(true) 183 | } 184 | // remoteVideoView?.setMirror(true) 185 | } 186 | 187 | override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { 188 | Log.w(TAG, "onRequestPermissionsResult: $requestCode $permissions $grantResults") 189 | when (requestCode) { 190 | CAMERA_AUDIO_PERMISSION_REQUEST -> { 191 | if (grantResults.isNotEmpty() && grantResults.first() == PackageManager.PERMISSION_GRANTED) { 192 | startVideoSession() 193 | } else { 194 | activity!!.finish() 195 | } 196 | return 197 | } 198 | } 199 | } 200 | 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/devjn/webrtcandroidfirebase/components/GLCircleDrawer.java: -------------------------------------------------------------------------------- 1 | package com.github.devjn.webrtcandroidfirebase.components; 2 | 3 | import android.opengl.GLES11Ext; 4 | import android.opengl.GLES20; 5 | 6 | import org.webrtc.GlShader; 7 | import org.webrtc.GlUtil; 8 | import org.webrtc.RendererCommon; 9 | 10 | import java.nio.FloatBuffer; 11 | import java.util.IdentityHashMap; 12 | import java.util.Map; 13 | 14 | 15 | /** 16 | * This is version of {@link RendererCommon.GlDrawer} which imitates circle by drawing pixels only located inside circle. 17 | * 18 | *

Created by @author Jahongir on 16-Feb-18 19 | * devjn@jn-arts.com 20 | * GLCircleDrawer 21 | */ 22 | public class GLCircleDrawer implements RendererCommon.GlDrawer { 23 | // clang-format off 24 | // Simple vertex shader, used for both YUV and OES. 25 | private static final String VERTEX_SHADER_STRING = 26 | "varying vec2 interp_tc;\n" 27 | + "attribute vec4 in_pos;\n" 28 | + "attribute vec4 in_tc;\n" 29 | + "\n" 30 | + "uniform mat4 texMatrix;\n" 31 | + "\n" 32 | + "void main() {\n" 33 | + " gl_Position = in_pos;\n" 34 | + " interp_tc = (texMatrix * in_tc).xy;\n" 35 | + "}\n"; 36 | private static final String YUV_FRAGMENT_SHADER_STRING = 37 | "precision mediump float;\n" 38 | + "varying vec2 interp_tc;\n" 39 | + "\n" 40 | + "uniform sampler2D y_tex;\n" 41 | + "uniform sampler2D u_tex;\n" 42 | + "uniform sampler2D v_tex;\n" 43 | + "\n" 44 | + "void main() {\n" 45 | + " vec2 p = -1.0 + 2.0 * interp_tc.xy;\n" 46 | + " p.x = p.x * 1.77 ;\n" 47 | + " float r = sqrt(dot(p,p));\n" 48 | + " if (r <= 1.0) {\n" 49 | // CSC according to http://www.fourcc.org/fccyvrgb.php 50 | + " float y = texture2D(y_tex, interp_tc).r;\n" 51 | + " float u = texture2D(u_tex, interp_tc).r - 0.5;\n" 52 | + " float v = texture2D(v_tex, interp_tc).r - 0.5;\n" 53 | + " gl_FragColor = vec4(y + 1.403 * v, " 54 | + " y - 0.344 * u - 0.714 * v, " 55 | + " y + 1.77 * u, 1);\n" 56 | + " } else {\n" 57 | + " gl_FragColor = vec4(0,0,0,0);\n" 58 | + " }\n" 59 | + "}\n"; 60 | 61 | private static final String RGB_FRAGMENT_SHADER_STRING = 62 | "precision mediump float;\n" 63 | + "varying vec2 interp_tc;\n" 64 | + "\n" 65 | + "uniform sampler2D rgb_tex;\n" 66 | + "\n" 67 | + "void main() {\n" 68 | + " vec2 p = -1.0 + 2.0 * interp_tc.xy;\n" 69 | + " float r = sqrt(dot(p,p));\n" 70 | + " gl_FragColor = ( (r < 1.0) ? texture2D(rgb_tex, interp_tc) : vec4(0,0,0,0));\n" 71 | +"}\n;"; 72 | 73 | private static final String OES_FRAGMENT_SHADER_STRING = 74 | "#extension GL_OES_EGL_image_external : require\n" 75 | + "precision mediump float;\n" 76 | + "varying vec2 interp_tc;\n" 77 | + "\n" 78 | + "uniform samplerExternalOES oes_tex;\n" 79 | + "\n" 80 | + "void main() {\n" 81 | + " vec2 p = -1.0 + 2.0 * interp_tc.xy;\n" 82 | + " float r = sqrt(dot(p,p));\n" 83 | + " gl_FragColor = ( (r < 1.0) ? texture2D(oes_tex, interp_tc) : vec4(0,0,0,0));\n" 84 | + "}\n"; 85 | // clang-format on 86 | // Vertex coordinates in Normalized Device Coordinates, i.e. (-1, -1) is bottom-left and (1, 1) is 87 | // top-right. 88 | private static final FloatBuffer FULL_RECTANGLE_BUF = GlUtil.createFloatBuffer(new float[] { 89 | -1.0f, -1.0f, // Bottom left. 90 | 1.0f, -1.0f, // Bottom right. 91 | -1.0f, 1.0f, // Top left. 92 | 1.0f, 1.0f, // Top right. 93 | }); 94 | // Texture coordinates - (0, 0) is bottom-left and (1, 1) is top-right. 95 | private static final FloatBuffer FULL_RECTANGLE_TEX_BUF = GlUtil.createFloatBuffer(new float[] { 96 | 0.0f, 0.0f, // Bottom left. 97 | 1.0f, 0.0f, // Bottom right. 98 | 0.0f, 1.0f, // Top left. 99 | 1.0f, 1.0f // Top right. 100 | }); 101 | private static class Shader { 102 | public final GlShader glShader; 103 | public final int texMatrixLocation; 104 | public Shader(String fragmentShader) { 105 | this.glShader = new GlShader(VERTEX_SHADER_STRING, fragmentShader); 106 | this.texMatrixLocation = glShader.getUniformLocation("texMatrix"); 107 | } 108 | } 109 | // The keys are one of the fragments shaders above. 110 | private final Map shaders = new IdentityHashMap<>(); 111 | 112 | /** 113 | * Draw an OES texture frame with specified texture transformation matrix. Required resources are 114 | * allocated at the first call to this function. 115 | */ 116 | @Override 117 | public void drawOes(int oesTextureId, float[] texMatrix, int frameWidth, int frameHeight, 118 | int viewportX, int viewportY, int viewportWidth, int viewportHeight) { 119 | prepareShader(OES_FRAGMENT_SHADER_STRING, texMatrix); 120 | GLES20.glActiveTexture(GLES20.GL_TEXTURE0); 121 | // updateTexImage() may be called from another thread in another EGL context, so we need to 122 | // bind/unbind the texture in each draw call so that GLES understads it's a new texture. 123 | GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, oesTextureId); 124 | drawRectangle(viewportX, viewportY, viewportWidth, viewportHeight); 125 | GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0); 126 | } 127 | /** 128 | * Draw a RGB(A) texture frame with specified texture transformation matrix. Required resources 129 | * are allocated at the first call to this function. 130 | */ 131 | @Override 132 | public void drawRgb(int textureId, float[] texMatrix, int frameWidth, int frameHeight, 133 | int viewportX, int viewportY, int viewportWidth, int viewportHeight) { 134 | prepareShader(RGB_FRAGMENT_SHADER_STRING, texMatrix); 135 | GLES20.glActiveTexture(GLES20.GL_TEXTURE0); 136 | GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId); 137 | drawRectangle(viewportX, viewportY, viewportWidth, viewportHeight); 138 | // Unbind the texture as a precaution. 139 | GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); 140 | } 141 | /** 142 | * Draw a YUV frame with specified texture transformation matrix. Required resources are 143 | * allocated at the first call to this function. 144 | */ 145 | @Override 146 | public void drawYuv(int[] yuvTextures, float[] texMatrix, int frameWidth, int frameHeight, 147 | int viewportX, int viewportY, int viewportWidth, int viewportHeight) { 148 | prepareShader(YUV_FRAGMENT_SHADER_STRING, texMatrix); 149 | // Bind the textures. 150 | for (int i = 0; i < 3; ++i) { 151 | GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i); 152 | GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yuvTextures[i]); 153 | } 154 | drawRectangle(viewportX, viewportY, viewportWidth, viewportHeight); 155 | // Unbind the textures as a precaution.. 156 | for (int i = 0; i < 3; ++i) { 157 | GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i); 158 | GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); 159 | } 160 | } 161 | private void drawRectangle(int x, int y, int width, int height) { 162 | // Draw quad. 163 | GLES20.glViewport(x, y, width, height); 164 | GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); 165 | } 166 | private void prepareShader(String fragmentShader, float[] texMatrix) { 167 | final Shader shader; 168 | if (shaders.containsKey(fragmentShader)) { 169 | shader = shaders.get(fragmentShader); 170 | } else { 171 | // Lazy allocation. 172 | shader = new Shader(fragmentShader); 173 | shaders.put(fragmentShader, shader); 174 | shader.glShader.useProgram(); 175 | // Initialize fragment shader uniform values. 176 | if (YUV_FRAGMENT_SHADER_STRING.equals(fragmentShader)) { 177 | GLES20.glUniform1i(shader.glShader.getUniformLocation("y_tex"), 0); 178 | GLES20.glUniform1i(shader.glShader.getUniformLocation("u_tex"), 1); 179 | GLES20.glUniform1i(shader.glShader.getUniformLocation("v_tex"), 2); 180 | } else if (RGB_FRAGMENT_SHADER_STRING.equals(fragmentShader)) { 181 | GLES20.glUniform1i(shader.glShader.getUniformLocation("rgb_tex"), 0); 182 | } else if (OES_FRAGMENT_SHADER_STRING.equals(fragmentShader)) { 183 | GLES20.glUniform1i(shader.glShader.getUniformLocation("oes_tex"), 0); 184 | } else { 185 | throw new IllegalStateException("Unknown fragment shader: " + fragmentShader); 186 | } 187 | GlUtil.checkNoGLES2Error("Initialize fragment shader uniform values."); 188 | // Initialize vertex shader attributes. 189 | shader.glShader.setVertexAttribArray("in_pos", 2, FULL_RECTANGLE_BUF); 190 | shader.glShader.setVertexAttribArray("in_tc", 2, FULL_RECTANGLE_TEX_BUF); 191 | } 192 | shader.glShader.useProgram(); 193 | // Copy the texture transformation matrix over. 194 | GLES20.glUniformMatrix4fv(shader.texMatrixLocation, 1, false, texMatrix, 0); 195 | } 196 | /** 197 | * Release all GLES resources. This needs to be done manually, otherwise the resources are leaked. 198 | */ 199 | @Override 200 | public void release() { 201 | for (Shader shader : shaders.values()) { 202 | shader.glShader.release(); 203 | } 204 | shaders.clear(); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/devjn/webrtcandroidfirebase/components/RoundedView.kt: -------------------------------------------------------------------------------- 1 | package com.github.devjn.webrtcandroidfirebase.components 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Path 6 | import android.util.AttributeSet 7 | import android.widget.FrameLayout 8 | 9 | 10 | /** 11 | * Created by @author Jahongir on 15-Feb-18 12 | * devjn@jn-arts.com 13 | * RoundedView 14 | */ 15 | public class RoundedView : FrameLayout { 16 | 17 | private val path = Path() 18 | 19 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) 20 | 21 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { 22 | super.onSizeChanged(w, h, oldw, oldh) 23 | // compute the path 24 | val halfWidth = w / 2f 25 | val halfHeight = h / 2f 26 | path.reset() 27 | path.addCircle(halfWidth, halfHeight, Math.min(halfWidth, halfHeight), Path.Direction.CW) 28 | path.close() 29 | 30 | } 31 | 32 | override fun dispatchDraw(canvas: Canvas) { 33 | val save = canvas.save() 34 | canvas.clipPath(path) 35 | super.dispatchDraw(canvas) 36 | canvas.restoreToCount(save) 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/devjn/webrtcandroidfirebase/videocall/AsyncSDPHelpers.kt: -------------------------------------------------------------------------------- 1 | package com.github.devjn.webrtcandroidfirebase.videocall 2 | 3 | import org.webrtc.SdpObserver 4 | import org.webrtc.SessionDescription 5 | 6 | /** 7 | * Created by Rafael on 01-23-18. 8 | */ 9 | 10 | 11 | interface SDPCreateResult 12 | 13 | data class SDPCreateSuccess(val descriptor: SessionDescription) : SDPCreateResult 14 | data class SDPCreateFailure(val reason: String?) : SDPCreateResult 15 | 16 | class SDPCreateCallback(private val callback: (SDPCreateResult) -> Unit) : SdpObserver { 17 | 18 | override fun onSetFailure(reason: String?) { } 19 | 20 | override fun onSetSuccess() { } 21 | 22 | override fun onCreateSuccess(descriptor: SessionDescription) = callback(SDPCreateSuccess(descriptor)) 23 | 24 | override fun onCreateFailure(reason: String?) = callback(SDPCreateFailure(reason)) 25 | } 26 | 27 | class SDPSetCallback(private val callback: (String?) -> Unit) : SdpObserver { 28 | 29 | override fun onSetFailure(reason: String?) = callback(reason) 30 | 31 | override fun onSetSuccess() = callback(null) 32 | 33 | override fun onCreateSuccess(descriptor: SessionDescription?) { } 34 | 35 | override fun onCreateFailure(reason: String?) { } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/devjn/webrtcandroidfirebase/videocall/FirebaseSignaler.kt: -------------------------------------------------------------------------------- 1 | package com.github.devjn.webrtcandroidfirebase.videocall 2 | 3 | import android.util.Log 4 | import com.github.devjn.webrtcandroidfirebase.FirebaseData 5 | import com.google.firebase.database.* 6 | 7 | 8 | /** 9 | * Created by @author Jahongir on 13-Feb-18 10 | * devjn@jn-arts.com 11 | * FirebaseSignaler 12 | */ 13 | 14 | enum class MessageType(val value: String) { 15 | SDPMessage("sdp"), 16 | ICEMessage("ice"), 17 | PeerLeft("peer-left"); 18 | 19 | override fun toString() = value 20 | } 21 | 22 | open class ClientMessage(@get:Exclude val type: MessageType) 23 | 24 | data class SDPMessage(var sdp: String = "") : ClientMessage(MessageType.SDPMessage) 25 | data class ICEMessage(var label: Int = 0, var id: String = "", var candidate: String = "") : ClientMessage(MessageType.ICEMessage) 26 | class PeerLeft : ClientMessage(MessageType.PeerLeft) 27 | 28 | class FirebaseSignaler(val callerID: String) { 29 | 30 | private val TAG = "FirebaseSignaler" 31 | 32 | private val database = FirebaseDatabase.getInstance() 33 | private val refToData = database.getReference(FirebaseData.getCallDataPath(callerID)) 34 | private val refToStatus = database.getReference(FirebaseData.getCallStatusPath(callerID)) 35 | private val refMyData = database.getReference(FirebaseData.getCallDataPath(FirebaseData.myID)) 36 | private val refMyStatus = database.getReference(FirebaseData.getCallStatusPath(FirebaseData.myID)) 37 | var messageHandler: ((ClientMessage) -> Unit)? = null 38 | 39 | private val dataListener: ChildEventListener = object : ChildEventListener { 40 | fun onMessage(dataSnapshot: DataSnapshot) { 41 | if (!dataSnapshot.exists()) return 42 | val type = dataSnapshot.key 43 | val clientMessage = 44 | when (type) { 45 | "sdp" -> 46 | dataSnapshot.getValue(SDPMessage::class.java) 47 | "ice" -> 48 | dataSnapshot.getValue(ICEMessage::class.java) 49 | else -> 50 | null 51 | } 52 | Log.i(TAG, "FirebaseSignaler: Decoded message as ${clientMessage?.type}") 53 | if (clientMessage != null) messageHandler!!.invoke(clientMessage) 54 | } 55 | 56 | override fun onChildAdded(dataSnapshot: DataSnapshot, p1: String?) { 57 | onMessage(dataSnapshot) 58 | } 59 | 60 | override fun onChildChanged(dataSnapshot: DataSnapshot, p1: String?) { 61 | onMessage(dataSnapshot) 62 | } 63 | 64 | override fun onChildMoved(dataSnapshot: DataSnapshot?, p1: String?) { 65 | } 66 | 67 | override fun onChildRemoved(dataSnapshot: DataSnapshot?) { 68 | } 69 | 70 | override fun onCancelled(e: DatabaseError) { 71 | Log.e(TAG, "databaseError:", e.toException()) 72 | } 73 | } 74 | 75 | private val statusListener = object : ValueEventListener { 76 | override fun onDataChange(dataSnapshot: DataSnapshot) { 77 | if (dataSnapshot.exists() && !dataSnapshot.getValue(Boolean::class.java)!!) 78 | messageHandler?.invoke(PeerLeft()) 79 | } 80 | 81 | override fun onCancelled(e: DatabaseError) { 82 | Log.e(TAG, "databaseError:", e.toException()) 83 | } 84 | } 85 | 86 | 87 | fun init() { 88 | listen() 89 | } 90 | 91 | private fun listen() { 92 | refMyData.onDisconnect().removeValue() 93 | refMyStatus.onDisconnect().setValue(false) 94 | refMyStatus.setValue(true) 95 | 96 | refToData.addChildEventListener(dataListener) 97 | refToStatus.addValueEventListener(statusListener) 98 | } 99 | 100 | private fun send(clientMessage: ClientMessage) { 101 | val reference = refMyData.child(clientMessage.type.value) 102 | reference.setValue(clientMessage).addOnCompleteListener { 103 | if (it.isSuccessful) { 104 | Log.i(TAG, "FirebaseSignaler: Sended succesfully ${clientMessage.type.value}") 105 | // reference.removeValue() 106 | } else 107 | Log.w(TAG, "Message of type '${clientMessage.type.value}' can't be sent to the server") 108 | } 109 | } 110 | 111 | fun close() { 112 | Log.w(TAG, "close") 113 | refToData.removeEventListener(dataListener) 114 | refToStatus.removeEventListener(statusListener) 115 | refMyData.removeValue() 116 | refMyStatus.setValue(false) 117 | // webSocket?.close(1000, null) 118 | } 119 | 120 | fun sendSDP(sdp: String) { 121 | Log.w(TAG, "sendSDP : " + sdp) 122 | send(SDPMessage(sdp)) 123 | } 124 | 125 | fun sendCandidate(label: Int, id: String, candidate: String) { 126 | Log.w(TAG, "sendCandidate : $label $id $candidate") 127 | send(ICEMessage(label, id, candidate)) 128 | } 129 | 130 | 131 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/devjn/webrtcandroidfirebase/videocall/VideoCallSession.kt: -------------------------------------------------------------------------------- 1 | package com.github.devjn.webrtcandroidfirebase.videocall 2 | 3 | 4 | import android.content.Context 5 | import android.util.Log 6 | import android.widget.Toast 7 | import com.github.devjn.webrtcandroidfirebase.FirebaseData 8 | import com.github.devjn.webrtcandroidfirebase.R 9 | import com.google.firebase.database.DataSnapshot 10 | import com.google.firebase.database.DatabaseError 11 | import com.google.firebase.database.ValueEventListener 12 | import org.webrtc.* 13 | import java.util.concurrent.Executors 14 | 15 | 16 | /** 17 | * Created by @author devjn on 02-10-18 18 | * devjn@jn-arts.com 19 | * VideoCallSession 20 | */ 21 | 22 | enum class VideoCallStatus(val label: Int, val color: Int) { 23 | UNKNOWN(R.string.status_unknown, R.color.colorUnknown), 24 | CONNECTING(R.string.status_connecting, R.color.colorConnecting), 25 | DIALING(R.string.status_dialing, R.color.colorMatching), 26 | FAILED(R.string.status_failed, R.color.colorFailed), 27 | CONNECTED(R.string.status_connected, R.color.colorConnected), 28 | FINISHED(R.string.status_finished, R.color.colorConnected); 29 | } 30 | 31 | data class VideoRenderers(private var localView: SurfaceViewRenderer?, private var remoteView: SurfaceViewRenderer?) { 32 | val localRenderer: (VideoRenderer.I420Frame) -> Unit = { f -> 33 | localView?.renderFrame(f) ?: sink(f) 34 | } 35 | // if (localView == null) this::sink else { f -> localView!!.renderFrame(f) } 36 | val remoteRenderer: (VideoRenderer.I420Frame) -> Unit = { f -> 37 | remoteView?.renderFrame(f) ?: sink(f) 38 | } 39 | // if (remoteView == null) this::sink else { f -> remoteView!!.renderFrame(f) } 40 | 41 | 42 | fun updateViewRenders(localView: SurfaceViewRenderer, remoteView: SurfaceViewRenderer) { 43 | this.localView = localView 44 | this.remoteView = remoteView 45 | } 46 | 47 | private fun sink(frame: VideoRenderer.I420Frame) { 48 | Log.w("VideoRenderer", "Missing surface view, dropping frame") 49 | VideoRenderer.renderFrameDone(frame) 50 | } 51 | } 52 | 53 | class VideoCallSession( 54 | private val context: Context, 55 | private val isOfferingPeer: Boolean, 56 | private val onStatusChangedListener: (VideoCallStatus) -> Unit, 57 | private val signaler: FirebaseSignaler, 58 | val videoRenderers: VideoRenderers) { 59 | 60 | private var peerConnection: PeerConnection? = null 61 | private var videoSource: VideoSource? = null 62 | private var audioSource: AudioSource? = null 63 | 64 | private var mediaStream: MediaStream? = null 65 | private var videoCapturer: VideoCapturer? = null 66 | private var videoTrack: VideoTrack? = null 67 | 68 | private val eglBase = EglBase.create() 69 | 70 | 71 | private val videoHeight = 1280 72 | private val videoWidth = 720 73 | private val videoFPS = 30 74 | 75 | val renderContext: EglBase.Context 76 | get() = eglBase.eglBaseContext 77 | 78 | class SimpleRTCEventHandler( 79 | private val onIceCandidateCb: (IceCandidate) -> Unit, 80 | private val onAddStreamCb: (MediaStream) -> Unit, 81 | private val onRemoveStreamCb: (MediaStream) -> Unit) : PeerConnection.Observer { 82 | 83 | override fun onIceCandidate(candidate: IceCandidate?) { 84 | if (candidate != null) onIceCandidateCb(candidate) 85 | } 86 | 87 | override fun onAddStream(stream: MediaStream?) { 88 | if (stream != null) onAddStreamCb(stream) 89 | } 90 | 91 | override fun onRemoveStream(stream: MediaStream?) { 92 | if (stream != null) onRemoveStreamCb(stream) 93 | } 94 | 95 | override fun onDataChannel(chan: DataChannel?) { 96 | Log.w(TAG, "onDataChannel: $chan") 97 | } 98 | 99 | override fun onIceConnectionReceivingChange(p0: Boolean) { 100 | Log.w(TAG, "onIceConnectionReceivingChange: $p0") 101 | } 102 | 103 | override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) { 104 | Log.w(TAG, "onIceConnectionChange: $newState") 105 | } 106 | 107 | override fun onIceGatheringChange(newState: PeerConnection.IceGatheringState?) { 108 | Log.w(TAG, "onIceGatheringChange: $newState") 109 | } 110 | 111 | override fun onSignalingChange(newState: PeerConnection.SignalingState?) { 112 | Log.w(TAG, "onSignalingChange: $newState") 113 | } 114 | 115 | override fun onIceCandidatesRemoved(candidates: Array?) { 116 | Log.w(TAG, "onIceCandidatesRemoved: $candidates") 117 | } 118 | 119 | override fun onRenegotiationNeeded() { 120 | Log.w(TAG, "onRenegotiationNeeded") 121 | } 122 | 123 | override fun onAddTrack(receiver: RtpReceiver?, streams: Array?) {} 124 | } 125 | 126 | private val factory: PeerConnectionFactory by lazy { 127 | //Initialize PeerConnectionFactory globals. 128 | val initializationOptions = PeerConnectionFactory.InitializationOptions.builder(context.applicationContext) 129 | .setEnableVideoHwAcceleration(true) 130 | .createInitializationOptions() 131 | PeerConnectionFactory.initialize(initializationOptions) 132 | 133 | //Create a new PeerConnectionFactory instance - using Hardware encoder and decoder. 134 | val options = PeerConnectionFactory.Options() 135 | options.networkIgnoreMask = 0 136 | val defaultVideoEncoderFactory = DefaultVideoEncoderFactory( 137 | renderContext, /* enableIntelVp8Encoder */true, /* enableH264HighProfile */true) 138 | val defaultVideoDecoderFactory = DefaultVideoDecoderFactory(renderContext) 139 | PeerConnectionFactory.builder() 140 | .setOptions(options) 141 | .setVideoEncoderFactory(defaultVideoEncoderFactory) 142 | .setVideoDecoderFactory(defaultVideoDecoderFactory) 143 | .createPeerConnectionFactory() 144 | } 145 | 146 | 147 | init { 148 | signaler.messageHandler = this::onMessage 149 | this.onStatusChangedListener(VideoCallStatus.DIALING) 150 | executor.execute(this::init) 151 | } 152 | 153 | private fun init() { 154 | val iceServers = arrayListOf( 155 | PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer() 156 | ) 157 | 158 | createPeerConnection(iceServers) 159 | setupMediaDevices() 160 | 161 | call() 162 | } 163 | 164 | private fun call() { 165 | val ref = FirebaseData.getCallStatusReference(signaler.callerID) 166 | ref.addValueEventListener(object : ValueEventListener { 167 | override fun onDataChange(dataSnapshot: DataSnapshot) { 168 | if (dataSnapshot.exists() && dataSnapshot.getValue(Boolean::class.java)!!) { 169 | ref.removeEventListener(this) 170 | onStatusChangedListener(VideoCallStatus.CONNECTING) 171 | start() 172 | } 173 | } 174 | 175 | override fun onCancelled(e: DatabaseError) { 176 | Log.e(TAG, "databaseError:", e.toException()) 177 | ref.removeEventListener(this) 178 | } 179 | }) 180 | } 181 | 182 | 183 | /** 184 | * Creating the local peerconnection instance 185 | */ 186 | private fun createPeerConnection(iceServers: List) { 187 | val rtcConfig = PeerConnection.RTCConfiguration(iceServers) 188 | rtcConfig.apply { 189 | // TCP candidates are only useful when connecting to a server that supports ICE-TCP. 190 | tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED 191 | bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE 192 | rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE 193 | continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY 194 | 195 | enableCpuOveruseDetection = true 196 | enableDtlsSrtp = true 197 | // Use ECDSA encryption. 198 | keyType = PeerConnection.KeyType.ECDSA 199 | } 200 | 201 | val rtcEvents = SimpleRTCEventHandler(this::handleLocalIceCandidate, this::addRemoteStream, this::removeRemoteStream) 202 | 203 | peerConnection = factory.createPeerConnection(rtcConfig, rtcEvents) 204 | } 205 | 206 | 207 | private fun start() { 208 | signaler.init() 209 | executor.execute(this::maybeCreateOffer) 210 | } 211 | 212 | private fun maybeCreateOffer() { 213 | if (isOfferingPeer) { 214 | peerConnection?.createOffer(SDPCreateCallback(this::createDescriptorCallback), defaultPcConstraints()) 215 | } 216 | } 217 | 218 | private fun defaultPcConstraints(): MediaConstraints { 219 | val pcConstraints = MediaConstraints() 220 | pcConstraints.optional.add(MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true")) 221 | pcConstraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true")) 222 | pcConstraints.mandatory.add(MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true")) 223 | return pcConstraints 224 | } 225 | 226 | private fun handleLocalIceCandidate(candidate: IceCandidate) { 227 | Log.w(TAG, "Local ICE candidate: $candidate") 228 | signaler.sendCandidate(candidate.sdpMLineIndex, candidate.sdpMid, candidate.sdp) 229 | } 230 | 231 | private fun addRemoteStream(stream: MediaStream) { 232 | onStatusChangedListener(VideoCallStatus.CONNECTED) 233 | Log.i(TAG, "Got remote stream: $stream") 234 | executor.execute { 235 | if (stream.videoTracks.isNotEmpty()) { 236 | val remoteVideoTrack = stream.videoTracks.first() 237 | remoteVideoTrack.setEnabled(true) 238 | remoteVideoTrack.addRenderer(VideoRenderer(videoRenderers.remoteRenderer)) 239 | } 240 | } 241 | } 242 | 243 | private fun removeRemoteStream(@Suppress("UNUSED_PARAMETER") _stream: MediaStream) { 244 | // We lost the stream, lets finish 245 | Log.w(TAG, "Bye") 246 | onStatusChangedListener(VideoCallStatus.FINISHED) 247 | } 248 | 249 | private fun handleRemoteCandidate(label: Int, id: String, strCandidate: String) { 250 | Log.i(TAG, "Got remote ICE candidate $strCandidate") 251 | executor.execute { 252 | val candidate = IceCandidate(id, label, strCandidate) 253 | peerConnection?.addIceCandidate(candidate) 254 | } 255 | } 256 | 257 | private fun setupMediaDevices() { 258 | mediaStream = factory.createLocalMediaStream(STREAM_LABEL) 259 | 260 | mediaStream?.addTrack(setupVideoTrack(isFront)) 261 | 262 | audioSource = factory.createAudioSource(createAudioConstraints()) 263 | val audioTrack = factory.createAudioTrack(AUDIO_TRACK_LABEL, audioSource) 264 | 265 | mediaStream?.addTrack(audioTrack) 266 | 267 | peerConnection?.addStream(mediaStream) 268 | } 269 | 270 | private fun setupVideoTrack(front: Boolean): VideoTrack? { 271 | val camera = if (useCamera2()) Camera2Enumerator(context) else Camera1Enumerator(false) 272 | 273 | videoCapturer = if (front) createFrontCameraCapturer(camera) else createBackCameraCapturer(camera) 274 | val videoSource = factory.createVideoSource(videoCapturer) 275 | videoCapturer?.startCapture(videoHeight, videoWidth, videoFPS) 276 | val videoRenderer = VideoRenderer(videoRenderers.localRenderer) 277 | 278 | videoTrack = factory.createVideoTrack(VIDEO_TRACK_LABEL, videoSource) 279 | videoTrack?.addRenderer(videoRenderer) 280 | return videoTrack 281 | } 282 | 283 | private fun createAudioConstraints(): MediaConstraints { 284 | val audioConstraints = MediaConstraints() 285 | audioConstraints.mandatory.add(MediaConstraints.KeyValuePair("googEchoCancellation", "true")) 286 | audioConstraints.mandatory.add(MediaConstraints.KeyValuePair("googAutoGainControl", "false")) 287 | audioConstraints.mandatory.add(MediaConstraints.KeyValuePair("googHighpassFilter", "false")) 288 | audioConstraints.mandatory.add(MediaConstraints.KeyValuePair("googNoiseSuppression", "true")) 289 | return audioConstraints 290 | } 291 | 292 | private fun handleRemoteDescriptor(sdp: String) { 293 | if (isOfferingPeer) { 294 | peerConnection?.setRemoteDescription(SDPSetCallback({ setError -> 295 | if (setError != null) { 296 | Log.e(TAG, "setRemoteDescription failed: $setError") 297 | } 298 | }), SessionDescription(SessionDescription.Type.ANSWER, sdp)) 299 | } else { 300 | peerConnection?.setRemoteDescription(SDPSetCallback({ setError -> 301 | if (setError != null) { 302 | Log.e(TAG, "setRemoteDescription failed: $setError") 303 | } else { 304 | peerConnection?.createAnswer(SDPCreateCallback(this::createDescriptorCallback), MediaConstraints()) 305 | } 306 | }), SessionDescription(SessionDescription.Type.OFFER, sdp)) 307 | } 308 | } 309 | 310 | private fun createDescriptorCallback(result: SDPCreateResult) { 311 | when (result) { 312 | is SDPCreateSuccess -> { 313 | peerConnection?.setLocalDescription(SDPSetCallback({ setResult -> 314 | Log.i(TAG, "SetLocalDescription: $setResult") 315 | }), result.descriptor) 316 | signaler.sendSDP(result.descriptor.description) 317 | } 318 | is SDPCreateFailure -> Log.e(TAG, "Error creating offer: ${result.reason}") 319 | } 320 | } 321 | 322 | private fun onMessage(message: ClientMessage) { 323 | when (message) { 324 | is SDPMessage -> { 325 | handleRemoteDescriptor(message.sdp) 326 | } 327 | is ICEMessage -> { 328 | handleRemoteCandidate(message.label, message.id, message.candidate) 329 | } 330 | is PeerLeft -> { 331 | onStatusChangedListener(VideoCallStatus.FINISHED) 332 | } 333 | } 334 | } 335 | 336 | 337 | fun terminate() { 338 | signaler.close() 339 | try { 340 | videoCapturer?.stopCapture() 341 | } catch (ex: Exception) { 342 | } 343 | 344 | videoCapturer?.dispose() 345 | videoSource?.dispose() 346 | 347 | audioSource?.dispose() 348 | 349 | peerConnection?.dispose() 350 | 351 | factory.dispose() 352 | 353 | eglBase.release() 354 | } 355 | 356 | private var isFront = true 357 | 358 | fun toggleCamera() { 359 | isFront = !isFront 360 | mediaStream?.removeTrack(videoTrack) 361 | videoTrack?.dispose() 362 | mediaStream?.addTrack(setupVideoTrack(isFront)) 363 | } 364 | 365 | private fun createFrontCameraCapturer(enumerator: CameraEnumerator): VideoCapturer? { 366 | val deviceNames = enumerator.deviceNames 367 | //find the front facing camera and return it. 368 | deviceNames 369 | .filter { enumerator.isFrontFacing(it) } 370 | .mapNotNull { enumerator.createCapturer(it, null) } 371 | .forEach { return it } 372 | 373 | return null 374 | } 375 | 376 | private fun createBackCameraCapturer(enumerator: CameraEnumerator): VideoCapturer? { 377 | // Front facing camera not found, try something else 378 | Logging.d(TAG, "Looking for other cameras.") 379 | val deviceNames = enumerator.deviceNames 380 | //find the front facing camera and return it. 381 | deviceNames 382 | .filter { enumerator.isBackFacing(it) } 383 | .mapNotNull { 384 | Logging.d(TAG, "Creating other camera capturer.") 385 | enumerator.createCapturer(it, null) 386 | } 387 | .forEach { return it } 388 | 389 | Toast.makeText(context, "No back camera found!", Toast.LENGTH_SHORT).show() 390 | return createFrontCameraCapturer(enumerator) 391 | } 392 | 393 | private fun useCamera2(): Boolean { 394 | return Camera2Enumerator.isSupported(context) 395 | } 396 | 397 | companion object { 398 | 399 | fun connect(context: Context, id: String, isOffer: Boolean, videoRenderers: VideoRenderers, callback: (VideoCallStatus) -> Unit): VideoCallSession { 400 | val firebaseHandler = FirebaseSignaler(id) 401 | return VideoCallSession(context, isOffer, callback, firebaseHandler, videoRenderers) 402 | } 403 | 404 | private const val STREAM_LABEL = "remoteStream" 405 | private const val VIDEO_TRACK_LABEL = "remoteVideoTrack" 406 | private const val AUDIO_TRACK_LABEL = "remoteAudioTrack" 407 | private const val TAG = "VideoCallSession" 408 | private val executor = Executors.newSingleThreadExecutor() 409 | } 410 | 411 | 412 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_action_camera_switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjn/WebRTCAndroidFirebase/3f1341c5bbae0ed048e7f4b52b08c1515d0256dd/app/src/main/res/drawable-hdpi/ic_action_camera_switch.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_call_end_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjn/WebRTCAndroidFirebase/3f1341c5bbae0ed048e7f4b52b08c1515d0256dd/app/src/main/res/drawable-hdpi/ic_call_end_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_action_camera_switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjn/WebRTCAndroidFirebase/3f1341c5bbae0ed048e7f4b52b08c1515d0256dd/app/src/main/res/drawable-mdpi/ic_action_camera_switch.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_call_end_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjn/WebRTCAndroidFirebase/3f1341c5bbae0ed048e7f4b52b08c1515d0256dd/app/src/main/res/drawable-mdpi/ic_call_end_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-nodpi/webrtc_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjn/WebRTCAndroidFirebase/3f1341c5bbae0ed048e7f4b52b08c1515d0256dd/app/src/main/res/drawable-nodpi/webrtc_logo.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_action_camera_switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjn/WebRTCAndroidFirebase/3f1341c5bbae0ed048e7f4b52b08c1515d0256dd/app/src/main/res/drawable-xhdpi/ic_action_camera_switch.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_call_end_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjn/WebRTCAndroidFirebase/3f1341c5bbae0ed048e7f4b52b08c1515d0256dd/app/src/main/res/drawable-xhdpi/ic_call_end_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_action_camera_switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjn/WebRTCAndroidFirebase/3f1341c5bbae0ed048e7f4b52b08c1515d0256dd/app/src/main/res/drawable-xxhdpi/ic_action_camera_switch.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_call_end_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjn/WebRTCAndroidFirebase/3f1341c5bbae0ed048e7f4b52b08c1515d0256dd/app/src/main/res/drawable-xxhdpi/ic_call_end_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_call_end_white_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devjn/WebRTCAndroidFirebase/3f1341c5bbae0ed048e7f4b52b08c1515d0256dd/app/src/main/res/drawable-xxxhdpi/ic_call_end_white_48dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_call_end.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 16 | 21 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 172 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_switch_camera.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_green.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/round_red.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/rounded_corner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_container.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_intro.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 21 | 22 |