├── .github ├── CODEOWNERS ├── dependabot.yml ├── pull_request_template.md └── workflows │ └── android.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── previews ├── docker-console-1.png ├── docker-console-2.png ├── preview0.png └── preview1.png ├── webrtc-android ├── .editorconfig ├── .gitignore ├── app │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ └── io │ │ │ └── getstream │ │ │ └── webrtc │ │ │ └── sample │ │ │ └── compose │ │ │ ├── MainActivity.kt │ │ │ ├── WebRTCApp.kt │ │ │ ├── ui │ │ │ ├── components │ │ │ │ ├── VideoRenderer.kt │ │ │ │ └── VideoTextureViewRenderer.kt │ │ │ ├── screens │ │ │ │ ├── stage │ │ │ │ │ └── StageScreen.kt │ │ │ │ └── video │ │ │ │ │ ├── CallMediaState.kt │ │ │ │ │ ├── FloatingVideoRenderer.kt │ │ │ │ │ ├── VideoCallControlAction.kt │ │ │ │ │ ├── VideoCallControls.kt │ │ │ │ │ └── VideoCallScreen.kt │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Shape.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ └── webrtc │ │ │ ├── SignalingClient.kt │ │ │ ├── audio │ │ │ ├── AudioDevice.kt │ │ │ ├── AudioDeviceChangeListener.kt │ │ │ ├── AudioFocusRequestWrapper.kt │ │ │ ├── AudioHandler.kt │ │ │ ├── AudioManagerAdapter.kt │ │ │ ├── AudioManagerAdapterImpl.kt │ │ │ └── AudioSwitch.kt │ │ │ ├── peer │ │ │ ├── StreamPeerConnection.kt │ │ │ ├── StreamPeerConnectionFactory.kt │ │ │ └── StreamPeerType.kt │ │ │ ├── sessions │ │ │ ├── WebRtcSessionManager.kt │ │ │ └── WebRtcSessionManagerImpl.kt │ │ │ └── utils │ │ │ ├── PeerConnectionUtils.kt │ │ │ ├── SDPUtils.kt │ │ │ └── stringify.kt │ │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_call_end.xml │ │ ├── ic_camera_flip.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_mic_off.xml │ │ ├── ic_mic_on.xml │ │ ├── ic_videocam_off.xml │ │ └── ic_videocam_on.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml ├── build.gradle.kts ├── buildSrc │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── Configurations.kt ├── dependabot.yml ├── gradle.properties ├── gradle │ ├── libs.versions.toml │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── spotless │ ├── copyright.kt │ ├── copyright.kts │ ├── copyright.xml │ └── spotless.gradle └── webrtc-backend ├── .gitignore ├── Dockerfile ├── build.gradle ├── docker-compose.yml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── resources ├── application.conf └── logback.xml ├── settings.gradle ├── src ├── Application.kt ├── SessionManager.kt └── TestWsClient.kt └── test └── ApplicationTest.kt /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # More details are here: https://help.github.com/articles/about-codeowners/ 5 | 6 | # The '*' pattern is global owners. 7 | # Not adding in this PR, but I'd like to try adding a global owner set with the entire team. 8 | # One interpretation of their docs is that global owners are added only if not removed 9 | # by a more local rule. 10 | 11 | # Order is important. The last matching pattern has the most precedence. 12 | # The folders are ordered as follows: 13 | 14 | # In each subsection folders are ordered first by depth, then alphabetically. 15 | # This should make it easy to add new rules without breaking existing ones. 16 | * @skydoves -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gradle" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | # Allow up to 10 open pull requests for pip dependencies 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### 🎯 Goal 2 | Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. 3 | 4 | ### 🛠 Implementation details 5 | Describe the implementation details for this Pull Request. 6 | 7 | ### ✍️ Explain examples 8 | Explain examples with code for this updates. 9 | 10 | ### Preparing a pull request for review 11 | Ensure your change is properly formatted by running: 12 | 13 | ```gradle 14 | $ ./gradlew spotlessApply 15 | ``` 16 | 17 | Please correct any failures before requesting a review. -------------------------------------------------------------------------------- /.github/workflows/android.yml: -------------------------------------------------------------------------------- 1 | name: Android CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | defaults: 13 | run: 14 | working-directory: ./webrtc-android 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: set up JDK 19 | uses: actions/setup-java@v1 20 | with: 21 | distribution: adopt 22 | java-version: 17 23 | 24 | - name: Create Local Properties 25 | run: | 26 | touch local.properties 27 | echo -e "127.0.0.1/rtc" >> local.properties 28 | 29 | - name: Cache Gradle and wrapper 30 | uses: actions/cache@v2 31 | with: 32 | path: | 33 | ~/.gradle/caches 34 | ~/.gradle/wrapper 35 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} 36 | restore-keys: | 37 | ${{ runner.os }}-gradle- 38 | 39 | - name: Make Gradle executable 40 | run: chmod +x ./gradlew 41 | 42 | - name: Build with Gradle 43 | run: ./gradlew build -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac 2 | *.DS_Store 3 | 4 | # Gradle files 5 | /.idea 6 | .gradle/ 7 | build/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | skydoves2@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute 2 | We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. 3 | 4 | ## Preparing a pull request for review 5 | Ensure your change is properly formatted by running: 6 | 7 | ```gradle 8 | ./gradlew spotlessApply 9 | ``` 10 | 11 | Please correct any failures before requesting a review. 12 | 13 | ## Code reviews 14 | All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) for more information on using pull requests. 15 | -------------------------------------------------------------------------------- /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 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![WebRTC in Jetpack Compose-1200x630](https://user-images.githubusercontent.com/24237865/211961074-8e01056c-a820-468b-bdca-3ab2570f783a.jpg) 2 | 3 |

WebRTC in Jetpack Compose


4 | 5 |

6 | License 7 | API 8 | Build Status 9 | Android Weekly 10 | Stream Feeds 11 |

12 | 13 | 14 | This project demonstrates [WebRTC protocol](https://getstream.io/glossary/webrtc-protocol/) to facilitate real-time video communications with Jetpack Compose. 15 | 16 | The purpose of this repository is to demonstrate below: 17 | - Implementing entire UI elements for real-time video communication with Jetpack Compose. 18 | - Performing real-time communication in background with Kotlin Coroutines. 19 | - Understanding the peer connection based on WebRTC. 20 | - Communicating with a signaling server to exchange peer connection information between clients. 21 | 22 | ## ✍️ Technical Content 23 | 24 | If you want to have a better grasp of how WebRTC works, such as basic concepts of WebRTC, relevant terminologies, and how to establish a peer-to-peer connection and communicate with the signaling server in Android, check out the articles below: 25 | 26 | - **[WebRTC for the Brave](https://getstream.io/resources/projects/webrtc/)**: This lesson consists of several modules aimed at helping developers better understand the concepts of WebRTC. From making your first call using peer-to-peer to deep technical breakdowns of common WebRTC architectures, we provide a step-by-step guide to understanding the nuances of the framework. 27 | - **[Building a Video Chat App: WebRTC on Android (Part1)](https://getstream.io/blog/webrtc-on-android/)** 28 | - **[Building a Video Chat App: WebRTC in Jetpack Compose (Part2)](https://getstream.io/blog/webrtc-jetpack-compose/)** 29 | - **[Pre-built Android WebRTC Library: Buld Your Own WebRTC Library for Android](https://getstream.io/resources/projects/webrtc/library/android/)** 30 | - **[HTTP, WebSocket, gRPC or WebRTC: Which Communication Protocol is Best For Your App?](https://getstream.io/blog/communication-protocols/)** 31 | - **[WebRTC Protocol: What is it and how does it work?](https://getstream.io/glossary/webrtc-protocol/)** 32 | 33 | If you'd like to get notified as we release future posts, join the **[watchers](https://github.com/GetStream/webrtc-in-jetpack-compose/watchers)** on GitHub or follow **[Stream](https://twitter.com/getstream_io)** on Twitter. You can also follow the __[author](https://github.com/skydoves)__ of this repository on GitHub. 34 | 35 | 36 | 37 | 38 | 39 | ## 🛥 Stream Chat and Voice & Video calling SDK 40 | If you’re interested in adding powerful In-App Messaging to your app, check out the __[Compose Chat SDK for Messaging](https://getstream.io/chat/sdk/compose/?utm_source=Github&utm_medium=Jaewoong_OSS&utm_content=Developer&utm_campaign=Github_Aug2023_Jaewoong_MeetingRoomCompose&utm_term=DevRelOss)__! We're also planning to release voice & video calling SDK very soon! Check out the **[Video & Voice Calling API on Stream's Global Edge Network](https://getstream.io/video/?utm_source=Github&utm_medium=Jaewoong_OSS&utm_content=Developer&utm_campaign=Github_Aug2023_Jaewoong_MeetingRoomCompose&utm_term=DevRelOss)**, if you want early access to our SDK. 41 | 42 | - [Stream Video SDK for Android on GitHub](https://github.com/getStream/stream-video-android) 43 | - [Video Call Tutorial](https://getstream.io/video/docs/android/tutorials/video-calling/?utm_source=Github&utm_medium=Jaewoong_OSS&utm_content=Developer&utm_campaign=Github_Aug2023_Jaewoong_MeetingRoomCompose&utm_term=DevRelOss) 44 | - [Audio Rooom Tutorial](https://getstream.io/video/docs/android/tutorials/audio-room/?utm_source=Github&utm_medium=Jaewoong_OSS&utm_content=Developer&utm_campaign=Github_Aug2023_Jaewoong_MeetingRoomCompose&utm_term=DevRelOss) 45 | - [Livestream Tutorial](https://getstream.io/video/docs/android/tutorials/livestream/?utm_source=Github&utm_medium=Jaewoong_OSS&utm_content=Developer&utm_campaign=Github_Aug2023_Jaewoong_MeetingRoomCompose&utm_term=DevRelOss) 46 | 47 | ## 📲 Download APK 48 | Go to the [Releases](https://github.com/GetStream/webrtc-in-jetpack-compose/releases) to download the latest APK. 49 | 50 | ## 📷 Previews 51 | 52 |

53 | drawing 54 | drawing 55 |

56 | 57 | ## 🛠 Tech Stacks & Open Source Libraries 58 | - Minimum SDK level 23. 59 | - 100% [Jetpack Compose](https://developer.android.com/jetpack/compose) based + [Coroutines](https://github.com/Kotlin/kotlinx.coroutines) + [Flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/) for asynchronous. 60 | - [WebRTC](https://webrtc.org/): To build real-time communication capabilities to your application that works on top of an open standard. 61 | - [Stream WebRTC Android](https://github.com/GetStream/stream-webrtc-android): A WebRTC pre-compiled library for Android reflects the recent WebRTC updates and supports functional UI components and extensions for Android and Jetpack Compose. 62 | - [Retrofit2 & OkHttp3](https://github.com/square/retrofit): Construct the REST APIs and paging network data. 63 | - [StreamLog](https://github.com/GetStream/stream-log): A lightweight and extensible logger library for Kotlin and Android. 64 | - [Ktor](https://github.com/ktorio/ktor): Building a signaling client websocket server. 65 | 66 | ## 💻 How to build the project? 67 | 68 | To build this project properly, you should follow the instructions below: 69 | 70 | 1. Run the [WebRTC backend server](https://github.com/GetStream/webrtc-in-jetpack-compose/tree/main/webrtc-backend). 71 | 2. Add the local ip address of your pc on the `local.properties` file on the project (Android Studio) like the below: 72 | 73 | ``` 74 | # You should change 192.168.1.123 to your local ip address, which is running the WebRTC backend server. 75 | SIGNALING_SERVER_IP_ADDRESS="ws://192.168.1.123:8080/rtc" 76 | ``` 77 | 78 | You will get your local IP address by typing the command below on your terminal: 79 | 80 | ``` 81 | ifconfig | grep "inet " | grep -Fv 127.0.0.1 | awk '{print $2}' 82 | ``` 83 | 84 | 3. Lastly, run the [WebRTC android](https://github.com/GetStream/webrtc-in-jetpack-compose/tree/main/webrtc-android) project on your multiple devices to test peer communication. 85 | 86 | ## 🤝 Contribution 87 | 88 | Most features are not completed except the chat feature, so that anyone can contribute and improve this project following the [Contributing Guideline](https://github.com/GetStream/webrtc-in-jetpack-compose/blob/main/CONTRIBUTING.md). 89 | 90 | ## Find this repository useful? 💙 91 | Support it by joining __[stargazers](https://github.com/GetStream/webrtc-in-jetpack-compose/stargazers)__ for this repository. :star:
92 | Also, follow __[maintainers](https://github.com/skydoves)__ on GitHub for our next creations! 🤩 93 | 94 | # License 95 | ```xml 96 | Copyright 2023 Stream.IO, Inc. All Rights Reserved. 97 | 98 | Licensed under the Apache License, Version 2.0 (the "License"); 99 | you may not use this file except in compliance with the License. 100 | You may obtain a copy of the License at 101 | 102 | http://www.apache.org/licenses/LICENSE-2.0 103 | 104 | Unless required by applicable law or agreed to in writing, software 105 | distributed under the License is distributed on an "AS IS" BASIS, 106 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 107 | See the License for the specific language governing permissions and 108 | limitations under the License. 109 | ``` 110 | -------------------------------------------------------------------------------- /previews/docker-console-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/webrtc-in-jetpack-compose/7a3c1a3d24058ee77e23ff0c86952dd2cd056253/previews/docker-console-1.png -------------------------------------------------------------------------------- /previews/docker-console-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/webrtc-in-jetpack-compose/7a3c1a3d24058ee77e23ff0c86952dd2cd056253/previews/docker-console-2.png -------------------------------------------------------------------------------- /previews/preview0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/webrtc-in-jetpack-compose/7a3c1a3d24058ee77e23ff0c86952dd2cd056253/previews/preview0.png -------------------------------------------------------------------------------- /previews/preview1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/webrtc-in-jetpack-compose/7a3c1a3d24058ee77e23ff0c86952dd2cd056253/previews/preview1.png -------------------------------------------------------------------------------- /webrtc-android/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | # Most of the standard properties are supported 4 | indent_size=2 5 | max_line_length=100 -------------------------------------------------------------------------------- /webrtc-android/.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | /.idea 18 | .gradle/ 19 | build/ 20 | 21 | # Local configuration file (sdk path, etc) 22 | local.properties 23 | 24 | # Proguard folder generated by Eclipse 25 | proguard/ 26 | 27 | # Log Files 28 | *.log 29 | 30 | # Android Studio Navigation editor temp files 31 | .navigation/ 32 | 33 | # Android Studio captures folder 34 | captures/ 35 | 36 | # Intellij 37 | *.iml 38 | .idea/workspace.xml 39 | .idea/tasks.xml 40 | .idea/gradle.xml 41 | .idea/dictionaries 42 | .idea/libraries 43 | app/.idea/ 44 | 45 | # Mac 46 | *.DS_Store 47 | 48 | # Keystore files 49 | *.jks 50 | 51 | # External native build folder generated in Android Studio 2.2 and later 52 | .externalNativeBuild 53 | 54 | # Google Services (e.g. APIs or Firebase) 55 | google-services.json 56 | *.google-services.json 57 | 58 | # Freeline 59 | freeline.py 60 | freeline/ 61 | freeline_project_description.json 62 | -------------------------------------------------------------------------------- /webrtc-android/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /webrtc-android/app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.io.FileInputStream 2 | import java.util.Properties 3 | 4 | @Suppress("DSL_SCOPE_VIOLATION") 5 | plugins { 6 | id(libs.plugins.android.application.get().pluginId) 7 | id(libs.plugins.kotlin.android.get().pluginId) 8 | id(libs.plugins.compose.compiler.get().pluginId) 9 | } 10 | 11 | val localProperties = Properties() 12 | localProperties.load(FileInputStream(rootProject.file("local.properties"))) 13 | 14 | android { 15 | namespace = "io.getstream.webrtc.sample.compose" 16 | compileSdk = Configurations.compileSdk 17 | 18 | defaultConfig { 19 | applicationId = "io.getstream.webrtc.sample.compose" 20 | minSdk = Configurations.minSdk 21 | targetSdk = Configurations.targetSdk 22 | versionCode = Configurations.versionCode 23 | versionName = Configurations.versionName 24 | 25 | buildConfigField( 26 | "String", 27 | "SIGNALING_SERVER_IP_ADDRESS", 28 | localProperties["SIGNALING_SERVER_IP_ADDRESS"].toString() 29 | ) 30 | } 31 | 32 | kotlinOptions { 33 | jvmTarget = "17" 34 | } 35 | 36 | buildFeatures { 37 | compose = true 38 | buildConfig = true 39 | } 40 | 41 | packagingOptions { 42 | resources { 43 | excludes.add("/META-INF/{AL2.0,LGPL2.1}") 44 | } 45 | } 46 | 47 | lint { 48 | abortOnError = false 49 | } 50 | } 51 | 52 | dependencies { 53 | // compose 54 | implementation(libs.androidx.activity.compose) 55 | implementation(libs.androidx.compose.runtime) 56 | implementation(libs.androidx.compose.ui) 57 | implementation(libs.androidx.compose.ui.tooling) 58 | implementation(libs.androidx.compose.material) 59 | implementation(libs.androidx.compose.foundation) 60 | implementation(libs.androidx.compose.foundation.layout) 61 | implementation(libs.androidx.compose.ui.tooling.preview) 62 | implementation(libs.androidx.compose.constraintlayout) 63 | 64 | // image loading 65 | implementation(libs.landscapist.glide) 66 | 67 | // webrtc 68 | implementation(libs.webrtc) 69 | implementation(libs.okhttp.logging) 70 | 71 | // coroutines 72 | implementation(libs.kotlinx.coroutines.android) 73 | 74 | // logger 75 | implementation(libs.stream.log) 76 | } -------------------------------------------------------------------------------- /webrtc-android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 39 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/MainActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose 18 | 19 | import android.Manifest 20 | import android.os.Bundle 21 | import androidx.activity.ComponentActivity 22 | import androidx.activity.compose.setContent 23 | import androidx.compose.foundation.layout.fillMaxSize 24 | import androidx.compose.material.MaterialTheme 25 | import androidx.compose.material.Surface 26 | import androidx.compose.runtime.CompositionLocalProvider 27 | import androidx.compose.runtime.collectAsState 28 | import androidx.compose.runtime.getValue 29 | import androidx.compose.runtime.mutableStateOf 30 | import androidx.compose.runtime.remember 31 | import androidx.compose.runtime.setValue 32 | import androidx.compose.ui.Modifier 33 | import io.getstream.webrtc.sample.compose.ui.screens.stage.StageScreen 34 | import io.getstream.webrtc.sample.compose.ui.screens.video.VideoCallScreen 35 | import io.getstream.webrtc.sample.compose.ui.theme.WebrtcSampleComposeTheme 36 | import io.getstream.webrtc.sample.compose.webrtc.SignalingClient 37 | import io.getstream.webrtc.sample.compose.webrtc.peer.StreamPeerConnectionFactory 38 | import io.getstream.webrtc.sample.compose.webrtc.sessions.LocalWebRtcSessionManager 39 | import io.getstream.webrtc.sample.compose.webrtc.sessions.WebRtcSessionManager 40 | import io.getstream.webrtc.sample.compose.webrtc.sessions.WebRtcSessionManagerImpl 41 | 42 | class MainActivity : ComponentActivity() { 43 | override fun onCreate(savedInstanceState: Bundle?) { 44 | super.onCreate(savedInstanceState) 45 | 46 | requestPermissions(arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO), 0) 47 | 48 | val sessionManager: WebRtcSessionManager = WebRtcSessionManagerImpl( 49 | context = this, 50 | signalingClient = SignalingClient(), 51 | peerConnectionFactory = StreamPeerConnectionFactory(this) 52 | ) 53 | 54 | setContent { 55 | WebrtcSampleComposeTheme { 56 | CompositionLocalProvider(LocalWebRtcSessionManager provides sessionManager) { 57 | // A surface container using the 'background' color from the theme 58 | Surface( 59 | modifier = Modifier.fillMaxSize(), 60 | color = MaterialTheme.colors.background 61 | ) { 62 | var onCallScreen by remember { mutableStateOf(false) } 63 | val state by sessionManager.signalingClient.sessionStateFlow.collectAsState() 64 | 65 | if (!onCallScreen) { 66 | StageScreen(state = state) { onCallScreen = true } 67 | } else { 68 | VideoCallScreen() 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/WebRTCApp.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose 18 | 19 | import android.app.Application 20 | import io.getstream.log.android.AndroidStreamLogger 21 | 22 | class WebRTCApp : Application() { 23 | 24 | override fun onCreate() { 25 | super.onCreate() 26 | 27 | AndroidStreamLogger.installOnDebuggableApp(this) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/ui/components/VideoRenderer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.ui.components 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.DisposableEffect 21 | import androidx.compose.runtime.MutableState 22 | import androidx.compose.runtime.getValue 23 | import androidx.compose.runtime.mutableStateOf 24 | import androidx.compose.runtime.remember 25 | import androidx.compose.runtime.setValue 26 | import androidx.compose.ui.Modifier 27 | import androidx.compose.ui.viewinterop.AndroidView 28 | import io.getstream.webrtc.sample.compose.webrtc.sessions.LocalWebRtcSessionManager 29 | import org.webrtc.RendererCommon 30 | import org.webrtc.VideoTrack 31 | 32 | /** 33 | * Renders a single video track based on the call state. 34 | * 35 | * @param videoTrack The track containing the video stream for a given participant. 36 | * @param modifier Modifier for styling. 37 | */ 38 | @Composable 39 | fun VideoRenderer( 40 | videoTrack: VideoTrack, 41 | modifier: Modifier = Modifier 42 | ) { 43 | val trackState: MutableState = remember { mutableStateOf(null) } 44 | var view: VideoTextureViewRenderer? by remember { mutableStateOf(null) } 45 | 46 | DisposableEffect(videoTrack) { 47 | onDispose { 48 | cleanTrack(view, trackState) 49 | } 50 | } 51 | 52 | val sessionManager = LocalWebRtcSessionManager.current 53 | AndroidView( 54 | factory = { context -> 55 | VideoTextureViewRenderer(context).apply { 56 | init( 57 | sessionManager.peerConnectionFactory.eglBaseContext, 58 | object : RendererCommon.RendererEvents { 59 | override fun onFirstFrameRendered() = Unit 60 | 61 | override fun onFrameResolutionChanged(p0: Int, p1: Int, p2: Int) = Unit 62 | } 63 | ) 64 | setupVideo(trackState, videoTrack, this) 65 | view = this 66 | } 67 | }, 68 | update = { v -> setupVideo(trackState, videoTrack, v) }, 69 | modifier = modifier 70 | ) 71 | } 72 | 73 | private fun cleanTrack( 74 | view: VideoTextureViewRenderer?, 75 | trackState: MutableState 76 | ) { 77 | view?.let { trackState.value?.removeSink(it) } 78 | trackState.value = null 79 | } 80 | 81 | private fun setupVideo( 82 | trackState: MutableState, 83 | track: VideoTrack, 84 | renderer: VideoTextureViewRenderer 85 | ) { 86 | if (trackState.value == track) { 87 | return 88 | } 89 | 90 | cleanTrack(renderer, trackState) 91 | 92 | trackState.value = track 93 | track.addSink(renderer) 94 | } 95 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/ui/components/VideoTextureViewRenderer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.ui.components 18 | 19 | import android.content.Context 20 | import android.content.res.Resources 21 | import android.graphics.SurfaceTexture 22 | import android.os.Handler 23 | import android.os.Looper 24 | import android.util.AttributeSet 25 | import android.view.TextureView 26 | import android.view.TextureView.SurfaceTextureListener 27 | import org.webrtc.EglBase 28 | import org.webrtc.EglRenderer 29 | import org.webrtc.GlRectDrawer 30 | import org.webrtc.RendererCommon.RendererEvents 31 | import org.webrtc.ThreadUtils 32 | import org.webrtc.VideoFrame 33 | import org.webrtc.VideoSink 34 | import java.util.concurrent.CountDownLatch 35 | 36 | /** 37 | * Custom [TextureView] used to render local/incoming videos on the screen. 38 | */ 39 | open class VideoTextureViewRenderer @JvmOverloads constructor( 40 | context: Context, 41 | attrs: AttributeSet? = null 42 | ) : TextureView(context, attrs), VideoSink, SurfaceTextureListener { 43 | 44 | /** 45 | * Cached resource name. 46 | */ 47 | private val resourceName: String = getResourceName() 48 | 49 | /** 50 | * Renderer used to render the video. 51 | */ 52 | private val eglRenderer: EglRenderer = EglRenderer(resourceName) 53 | 54 | /** 55 | * Callback used for reporting render events. 56 | */ 57 | private var rendererEvents: RendererEvents? = null 58 | 59 | /** 60 | * Handler to access the UI thread. 61 | */ 62 | private val uiThreadHandler = Handler(Looper.getMainLooper()) 63 | 64 | /** 65 | * Whether the first frame has been rendered or not. 66 | */ 67 | private var isFirstFrameRendered = false 68 | 69 | /** 70 | * The rotated [VideoFrame] width. 71 | */ 72 | private var rotatedFrameWidth = 0 73 | 74 | /** 75 | * The rotated [VideoFrame] height. 76 | */ 77 | private var rotatedFrameHeight = 0 78 | 79 | /** 80 | * The rotated [VideoFrame] rotation. 81 | */ 82 | private var frameRotation = 0 83 | 84 | init { 85 | surfaceTextureListener = this 86 | } 87 | 88 | /** 89 | * Called when a new frame is received. Sends the frame to be rendered. 90 | * 91 | * @param videoFrame The [VideoFrame] received from WebRTC connection to draw on the screen. 92 | */ 93 | override fun onFrame(videoFrame: VideoFrame) { 94 | eglRenderer.onFrame(videoFrame) 95 | updateFrameData(videoFrame) 96 | } 97 | 98 | /** 99 | * Updates the frame data and notifies [rendererEvents] about the changes. 100 | */ 101 | private fun updateFrameData(videoFrame: VideoFrame) { 102 | if (isFirstFrameRendered) { 103 | rendererEvents?.onFirstFrameRendered() 104 | isFirstFrameRendered = true 105 | } 106 | 107 | if (videoFrame.rotatedWidth != rotatedFrameWidth || 108 | videoFrame.rotatedHeight != rotatedFrameHeight || 109 | videoFrame.rotation != frameRotation 110 | ) { 111 | rotatedFrameWidth = videoFrame.rotatedWidth 112 | rotatedFrameHeight = videoFrame.rotatedHeight 113 | frameRotation = videoFrame.rotation 114 | 115 | uiThreadHandler.post { 116 | rendererEvents?.onFrameResolutionChanged( 117 | rotatedFrameWidth, 118 | rotatedFrameHeight, 119 | frameRotation 120 | ) 121 | } 122 | } 123 | } 124 | 125 | /** 126 | * After the view is laid out we need to set the correct layout aspect ratio to the renderer so that the image 127 | * is scaled correctly. 128 | */ 129 | override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { 130 | eglRenderer.setLayoutAspectRatio((right - left) / (bottom.toFloat() - top)) 131 | } 132 | 133 | /** 134 | * Initialise the renderer. Should be called from the main thread. 135 | * 136 | * @param sharedContext [EglBase.Context] 137 | * @param rendererEvents Sets the render event listener. 138 | */ 139 | fun init( 140 | sharedContext: EglBase.Context, 141 | rendererEvents: RendererEvents 142 | ) { 143 | ThreadUtils.checkIsOnMainThread() 144 | this.rendererEvents = rendererEvents 145 | eglRenderer.init(sharedContext, EglBase.CONFIG_PLAIN, GlRectDrawer()) 146 | } 147 | 148 | /** 149 | * [SurfaceTextureListener] callback that lets us know when a surface texture is ready and we can draw on it. 150 | */ 151 | override fun onSurfaceTextureAvailable(surfaceTexture: SurfaceTexture, width: Int, height: Int) { 152 | eglRenderer.createEglSurface(surfaceTexture) 153 | } 154 | 155 | /** 156 | * [SurfaceTextureListener] callback that lets us know when a surface texture is destroyed we need to stop drawing 157 | * on it. 158 | */ 159 | override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean { 160 | val completionLatch = CountDownLatch(1) 161 | eglRenderer.releaseEglSurface { completionLatch.countDown() } 162 | ThreadUtils.awaitUninterruptibly(completionLatch) 163 | return true 164 | } 165 | 166 | override fun onSurfaceTextureSizeChanged( 167 | surfaceTexture: SurfaceTexture, 168 | width: Int, 169 | height: Int 170 | ) { 171 | } 172 | 173 | override fun onSurfaceTextureUpdated(surfaceTexture: SurfaceTexture) {} 174 | 175 | override fun onDetachedFromWindow() { 176 | eglRenderer.release() 177 | super.onDetachedFromWindow() 178 | } 179 | 180 | private fun getResourceName(): String { 181 | return try { 182 | resources.getResourceEntryName(id) + ": " 183 | } catch (e: Resources.NotFoundException) { 184 | "" 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/ui/screens/stage/StageScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.ui.screens.stage 18 | 19 | import androidx.compose.foundation.layout.Box 20 | import androidx.compose.foundation.layout.fillMaxSize 21 | import androidx.compose.material.Button 22 | import androidx.compose.material.Text 23 | import androidx.compose.runtime.Composable 24 | import androidx.compose.runtime.getValue 25 | import androidx.compose.runtime.mutableStateOf 26 | import androidx.compose.runtime.remember 27 | import androidx.compose.runtime.setValue 28 | import androidx.compose.ui.Alignment 29 | import androidx.compose.ui.Modifier 30 | import androidx.compose.ui.res.stringResource 31 | import androidx.compose.ui.text.font.FontWeight 32 | import androidx.compose.ui.unit.sp 33 | import io.getstream.webrtc.sample.compose.R 34 | import io.getstream.webrtc.sample.compose.webrtc.WebRTCSessionState 35 | 36 | @Composable 37 | fun StageScreen( 38 | state: WebRTCSessionState, 39 | onJoinCall: () -> Unit 40 | ) { 41 | Box(modifier = Modifier.fillMaxSize()) { 42 | var enabledCall by remember { mutableStateOf(false) } 43 | 44 | val text = when (state) { 45 | WebRTCSessionState.Offline -> { 46 | enabledCall = false 47 | stringResource(id = R.string.button_start_session) 48 | } 49 | WebRTCSessionState.Impossible -> { 50 | enabledCall = false 51 | stringResource(id = R.string.session_impossible) 52 | } 53 | WebRTCSessionState.Ready -> { 54 | enabledCall = true 55 | stringResource(id = R.string.session_ready) 56 | } 57 | WebRTCSessionState.Creating -> { 58 | enabledCall = true 59 | stringResource(id = R.string.session_creating) 60 | } 61 | WebRTCSessionState.Active -> { 62 | enabledCall = false 63 | stringResource(id = R.string.session_active) 64 | } 65 | } 66 | 67 | Button( 68 | modifier = Modifier.align(Alignment.Center), 69 | enabled = enabledCall, 70 | onClick = { onJoinCall.invoke() } 71 | ) { 72 | Text( 73 | text = text, 74 | fontSize = 26.sp, 75 | fontWeight = FontWeight.Bold 76 | ) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/ui/screens/video/CallMediaState.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.ui.screens.video 18 | 19 | data class CallMediaState( 20 | val isMicrophoneEnabled: Boolean = true, 21 | val isCameraEnabled: Boolean = true 22 | ) 23 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/ui/screens/video/FloatingVideoRenderer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.ui.screens.video 18 | 19 | import androidx.compose.animation.core.animateOffsetAsState 20 | import androidx.compose.foundation.gestures.detectDragGestures 21 | import androidx.compose.foundation.layout.PaddingValues 22 | import androidx.compose.foundation.layout.fillMaxSize 23 | import androidx.compose.foundation.layout.offset 24 | import androidx.compose.foundation.layout.padding 25 | import androidx.compose.foundation.shape.RoundedCornerShape 26 | import androidx.compose.material.Card 27 | import androidx.compose.runtime.Composable 28 | import androidx.compose.runtime.LaunchedEffect 29 | import androidx.compose.runtime.getValue 30 | import androidx.compose.runtime.mutableStateOf 31 | import androidx.compose.runtime.remember 32 | import androidx.compose.runtime.setValue 33 | import androidx.compose.ui.Modifier 34 | import androidx.compose.ui.draw.clip 35 | import androidx.compose.ui.geometry.Offset 36 | import androidx.compose.ui.input.pointer.pointerInput 37 | import androidx.compose.ui.layout.onGloballyPositioned 38 | import androidx.compose.ui.platform.LocalDensity 39 | import androidx.compose.ui.unit.Density 40 | import androidx.compose.ui.unit.IntOffset 41 | import androidx.compose.ui.unit.IntSize 42 | import androidx.compose.ui.unit.LayoutDirection 43 | import androidx.compose.ui.unit.dp 44 | import io.getstream.webrtc.sample.compose.ui.components.VideoRenderer 45 | import org.webrtc.VideoTrack 46 | 47 | /** 48 | * Represents a floating item used to feature a participant video, usually the local participant. 49 | * 50 | * @param parentBounds Bounds of the parent, used to constrain the component to the parent bounds, 51 | * when dragging the floating UI around the screen. 52 | * @param modifier Modifier for styling. 53 | */ 54 | @Composable 55 | fun FloatingVideoRenderer( 56 | videoTrack: VideoTrack, 57 | parentBounds: IntSize, 58 | paddingValues: PaddingValues, 59 | modifier: Modifier = Modifier 60 | ) { 61 | var videoSize by remember { mutableStateOf(IntSize(0, 0)) } 62 | var offsetX by remember { mutableStateOf(0f) } 63 | var offsetY by remember { mutableStateOf(0f) } 64 | val offset by animateOffsetAsState(targetValue = Offset(offsetX, offsetY)) 65 | val density = LocalDensity.current 66 | 67 | LaunchedEffect(parentBounds.width) { 68 | offsetX = 0f 69 | offsetY = 0f 70 | } 71 | 72 | val paddingOffset = density.run { 16.dp.toPx() } 73 | 74 | Card( 75 | elevation = 8.dp, 76 | modifier = Modifier 77 | .offset { IntOffset(offset.x.toInt(), offset.y.toInt()) } 78 | .pointerInput(parentBounds) { 79 | detectDragGestures { change, dragAmount -> 80 | change.consume() 81 | 82 | val newOffsetX = (offsetX + dragAmount.x) 83 | .coerceAtLeast( 84 | -calculateHorizontalOffsetBounds( 85 | parentBounds = parentBounds, 86 | paddingValues = paddingValues, 87 | floatingVideoSize = videoSize, 88 | density = density, 89 | offset = paddingOffset * 2 90 | ) 91 | ) 92 | .coerceAtMost( 93 | 0f 94 | ) 95 | 96 | val newOffsetY = (offsetY + dragAmount.y) 97 | .coerceAtLeast(0f) 98 | .coerceAtMost( 99 | calculateVerticalOffsetBounds( 100 | parentBounds = parentBounds, 101 | paddingValues = paddingValues, 102 | floatingVideoSize = videoSize, 103 | density = density, 104 | offset = paddingOffset * 2 105 | ) 106 | ) 107 | 108 | offsetX = newOffsetX 109 | offsetY = newOffsetY 110 | } 111 | } 112 | .then(modifier) 113 | .padding(16.dp) 114 | .onGloballyPositioned { videoSize = it.size }, 115 | shape = RoundedCornerShape(16.dp) 116 | ) { 117 | VideoRenderer( 118 | modifier = Modifier 119 | .fillMaxSize() 120 | .clip(RoundedCornerShape(16.dp)), 121 | videoTrack = videoTrack 122 | ) 123 | } 124 | } 125 | 126 | private fun calculateHorizontalOffsetBounds( 127 | parentBounds: IntSize, 128 | paddingValues: PaddingValues, 129 | floatingVideoSize: IntSize, 130 | density: Density, 131 | offset: Float 132 | ): Float { 133 | val rightPadding = 134 | density.run { paddingValues.calculateRightPadding(LayoutDirection.Ltr).toPx() } 135 | 136 | return parentBounds.width - rightPadding - floatingVideoSize.width - offset 137 | } 138 | 139 | private fun calculateVerticalOffsetBounds( 140 | parentBounds: IntSize, 141 | paddingValues: PaddingValues, 142 | floatingVideoSize: IntSize, 143 | density: Density, 144 | offset: Float 145 | ): Float { 146 | val bottomPadding = 147 | density.run { paddingValues.calculateBottomPadding().toPx() } 148 | 149 | return parentBounds.height - bottomPadding - floatingVideoSize.height - offset 150 | } 151 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/ui/screens/video/VideoCallControlAction.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.ui.screens.video 18 | 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.graphics.Color 21 | import androidx.compose.ui.graphics.painter.Painter 22 | import androidx.compose.ui.res.painterResource 23 | import io.getstream.webrtc.sample.compose.R 24 | import io.getstream.webrtc.sample.compose.ui.theme.Disabled 25 | import io.getstream.webrtc.sample.compose.ui.theme.Primary 26 | 27 | sealed class CallAction { 28 | data class ToggleMicroPhone( 29 | val isEnabled: Boolean 30 | ) : CallAction() 31 | 32 | data class ToggleCamera( 33 | val isEnabled: Boolean 34 | ) : CallAction() 35 | 36 | object FlipCamera : CallAction() 37 | 38 | object LeaveCall : CallAction() 39 | } 40 | 41 | data class VideoCallControlAction( 42 | val icon: Painter, 43 | val iconTint: Color, 44 | val background: Color, 45 | val callAction: CallAction 46 | ) 47 | 48 | @Composable 49 | fun buildDefaultCallControlActions( 50 | callMediaState: CallMediaState 51 | ): List { 52 | val microphoneIcon = 53 | painterResource( 54 | id = if (callMediaState.isMicrophoneEnabled) { 55 | R.drawable.ic_mic_on 56 | } else { 57 | R.drawable.ic_mic_off 58 | } 59 | ) 60 | 61 | val cameraIcon = painterResource( 62 | id = if (callMediaState.isCameraEnabled) { 63 | R.drawable.ic_videocam_on 64 | } else { 65 | R.drawable.ic_videocam_off 66 | } 67 | ) 68 | 69 | return listOf( 70 | VideoCallControlAction( 71 | icon = microphoneIcon, 72 | iconTint = Color.White, 73 | background = Primary, 74 | callAction = CallAction.ToggleMicroPhone(callMediaState.isMicrophoneEnabled) 75 | ), 76 | VideoCallControlAction( 77 | icon = cameraIcon, 78 | iconTint = Color.White, 79 | background = Primary, 80 | callAction = CallAction.ToggleCamera(callMediaState.isCameraEnabled) 81 | ), 82 | VideoCallControlAction( 83 | icon = painterResource(id = R.drawable.ic_camera_flip), 84 | iconTint = Color.White, 85 | background = Primary, 86 | callAction = CallAction.FlipCamera 87 | ), 88 | VideoCallControlAction( 89 | icon = painterResource(id = R.drawable.ic_call_end), 90 | iconTint = Color.White, 91 | background = Disabled, 92 | callAction = CallAction.LeaveCall 93 | ) 94 | ) 95 | } 96 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/ui/screens/video/VideoCallControls.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.ui.screens.video 18 | 19 | import androidx.compose.foundation.background 20 | import androidx.compose.foundation.clickable 21 | import androidx.compose.foundation.layout.Arrangement 22 | import androidx.compose.foundation.layout.Box 23 | import androidx.compose.foundation.layout.padding 24 | import androidx.compose.foundation.layout.size 25 | import androidx.compose.foundation.lazy.LazyRow 26 | import androidx.compose.foundation.lazy.items 27 | import androidx.compose.foundation.shape.CircleShape 28 | import androidx.compose.material.Icon 29 | import androidx.compose.runtime.Composable 30 | import androidx.compose.ui.Alignment 31 | import androidx.compose.ui.Modifier 32 | import androidx.compose.ui.draw.clip 33 | import androidx.compose.ui.unit.dp 34 | 35 | @Composable 36 | fun VideoCallControls( 37 | modifier: Modifier, 38 | callMediaState: CallMediaState, 39 | actions: List = buildDefaultCallControlActions(callMediaState = callMediaState), 40 | onCallAction: (CallAction) -> Unit 41 | ) { 42 | LazyRow( 43 | modifier = modifier.padding(bottom = 12.dp), 44 | verticalAlignment = Alignment.CenterVertically, 45 | horizontalArrangement = Arrangement.SpaceEvenly 46 | ) { 47 | items(actions) { action -> 48 | Box( 49 | modifier = Modifier 50 | .size(56.dp) 51 | .clip(CircleShape) 52 | .background(action.background) 53 | ) { 54 | Icon( 55 | modifier = Modifier 56 | .padding(10.dp) 57 | .align(Alignment.Center) 58 | .clickable { onCallAction(action.callAction) }, 59 | tint = action.iconTint, 60 | painter = action.icon, 61 | contentDescription = null 62 | ) 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/ui/screens/video/VideoCallScreen.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.ui.screens.video 18 | 19 | import android.app.Activity 20 | import androidx.compose.foundation.layout.Box 21 | import androidx.compose.foundation.layout.PaddingValues 22 | import androidx.compose.foundation.layout.fillMaxSize 23 | import androidx.compose.foundation.layout.fillMaxWidth 24 | import androidx.compose.foundation.layout.size 25 | import androidx.compose.foundation.shape.RoundedCornerShape 26 | import androidx.compose.runtime.Composable 27 | import androidx.compose.runtime.LaunchedEffect 28 | import androidx.compose.runtime.collectAsState 29 | import androidx.compose.runtime.getValue 30 | import androidx.compose.runtime.mutableStateOf 31 | import androidx.compose.runtime.remember 32 | import androidx.compose.runtime.setValue 33 | import androidx.compose.ui.Alignment 34 | import androidx.compose.ui.Modifier 35 | import androidx.compose.ui.draw.clip 36 | import androidx.compose.ui.layout.onSizeChanged 37 | import androidx.compose.ui.platform.LocalContext 38 | import androidx.compose.ui.unit.IntSize 39 | import androidx.compose.ui.unit.dp 40 | import io.getstream.webrtc.sample.compose.ui.components.VideoRenderer 41 | import io.getstream.webrtc.sample.compose.webrtc.sessions.LocalWebRtcSessionManager 42 | 43 | @Composable 44 | fun VideoCallScreen() { 45 | val sessionManager = LocalWebRtcSessionManager.current 46 | 47 | LaunchedEffect(key1 = Unit) { 48 | sessionManager.onSessionScreenReady() 49 | } 50 | 51 | Box( 52 | modifier = Modifier.fillMaxSize() 53 | ) { 54 | var parentSize: IntSize by remember { mutableStateOf(IntSize(0, 0)) } 55 | 56 | val remoteVideoTrackState by sessionManager.remoteVideoTrackFlow.collectAsState(null) 57 | val remoteVideoTrack = remoteVideoTrackState 58 | 59 | val localVideoTrackState by sessionManager.localVideoTrackFlow.collectAsState(null) 60 | val localVideoTrack = localVideoTrackState 61 | 62 | var callMediaState by remember { mutableStateOf(CallMediaState()) } 63 | 64 | if (remoteVideoTrack != null) { 65 | VideoRenderer( 66 | videoTrack = remoteVideoTrack, 67 | modifier = Modifier 68 | .fillMaxSize() 69 | .onSizeChanged { parentSize = it } 70 | ) 71 | } 72 | 73 | if (localVideoTrack != null && callMediaState.isCameraEnabled) { 74 | FloatingVideoRenderer( 75 | modifier = Modifier 76 | .size(width = 150.dp, height = 210.dp) 77 | .clip(RoundedCornerShape(16.dp)) 78 | .align(Alignment.TopEnd), 79 | videoTrack = localVideoTrack, 80 | parentBounds = parentSize, 81 | paddingValues = PaddingValues(0.dp) 82 | ) 83 | } 84 | 85 | val activity = (LocalContext.current as? Activity) 86 | 87 | VideoCallControls( 88 | modifier = Modifier 89 | .fillMaxWidth() 90 | .align(Alignment.BottomCenter), 91 | callMediaState = callMediaState, 92 | onCallAction = { 93 | when (it) { 94 | is CallAction.ToggleMicroPhone -> { 95 | val enabled = callMediaState.isMicrophoneEnabled.not() 96 | callMediaState = callMediaState.copy(isMicrophoneEnabled = enabled) 97 | sessionManager.enableMicrophone(enabled) 98 | } 99 | is CallAction.ToggleCamera -> { 100 | val enabled = callMediaState.isCameraEnabled.not() 101 | callMediaState = callMediaState.copy(isCameraEnabled = enabled) 102 | sessionManager.enableCamera(enabled) 103 | } 104 | CallAction.FlipCamera -> sessionManager.flipCamera() 105 | CallAction.LeaveCall -> { 106 | sessionManager.disconnect() 107 | activity?.finish() 108 | } 109 | } 110 | } 111 | ) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.ui.theme 18 | 19 | import androidx.compose.ui.graphics.Color 20 | 21 | val Purple200 = Color(0xFFBB86FC) 22 | val Purple500 = Color(0xFF6200EE) 23 | val Purple700 = Color(0xFF3700B3) 24 | val Teal200 = Color(0xFF03DAC5) 25 | val Primary = Color(0xFF337EFF) 26 | val Disabled = Color(0xFFFF1A24) 27 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.ui.theme 18 | 19 | import androidx.compose.foundation.shape.RoundedCornerShape 20 | import androidx.compose.material.Shapes 21 | import androidx.compose.ui.unit.dp 22 | 23 | val Shapes = Shapes( 24 | small = RoundedCornerShape(4.dp), 25 | medium = RoundedCornerShape(4.dp), 26 | large = RoundedCornerShape(0.dp) 27 | ) 28 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.ui.theme 18 | 19 | import androidx.compose.foundation.isSystemInDarkTheme 20 | import androidx.compose.material.MaterialTheme 21 | import androidx.compose.material.darkColors 22 | import androidx.compose.material.lightColors 23 | import androidx.compose.runtime.Composable 24 | 25 | private val DarkColorPalette = darkColors( 26 | primary = Purple200, 27 | primaryVariant = Purple700, 28 | secondary = Teal200 29 | ) 30 | 31 | private val LightColorPalette = lightColors( 32 | primary = Purple500, 33 | primaryVariant = Purple700, 34 | secondary = Teal200 35 | 36 | /* Other default colors to override 37 | background = Color.White, 38 | surface = Color.White, 39 | onPrimary = Color.White, 40 | onSecondary = Color.Black, 41 | onBackground = Color.Black, 42 | onSurface = Color.Black, 43 | */ 44 | ) 45 | 46 | @Composable 47 | fun WebrtcSampleComposeTheme( 48 | darkTheme: Boolean = isSystemInDarkTheme(), 49 | content: @Composable () -> Unit 50 | ) { 51 | val colors = if (darkTheme) { 52 | DarkColorPalette 53 | } else { 54 | LightColorPalette 55 | } 56 | 57 | MaterialTheme( 58 | colors = colors, 59 | typography = Typography, 60 | shapes = Shapes, 61 | content = content 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.ui.theme 18 | 19 | import androidx.compose.material.Typography 20 | import androidx.compose.ui.text.TextStyle 21 | import androidx.compose.ui.text.font.FontFamily 22 | import androidx.compose.ui.text.font.FontWeight 23 | import androidx.compose.ui.unit.sp 24 | 25 | // Set of Material typography styles to start with 26 | val Typography = Typography( 27 | body1 = TextStyle( 28 | fontFamily = FontFamily.Default, 29 | fontWeight = FontWeight.Normal, 30 | fontSize = 16.sp 31 | ) 32 | /* Other default text styles to override 33 | button = TextStyle( 34 | fontFamily = FontFamily.Default, 35 | fontWeight = FontWeight.W500, 36 | fontSize = 14.sp 37 | ), 38 | caption = TextStyle( 39 | fontFamily = FontFamily.Default, 40 | fontWeight = FontWeight.Normal, 41 | fontSize = 12.sp 42 | ) 43 | */ 44 | ) 45 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/webrtc/SignalingClient.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.webrtc 18 | 19 | import io.getstream.log.taggedLogger 20 | import io.getstream.webrtc.sample.compose.BuildConfig 21 | import kotlinx.coroutines.CoroutineScope 22 | import kotlinx.coroutines.Dispatchers 23 | import kotlinx.coroutines.SupervisorJob 24 | import kotlinx.coroutines.cancel 25 | import kotlinx.coroutines.flow.MutableSharedFlow 26 | import kotlinx.coroutines.flow.MutableStateFlow 27 | import kotlinx.coroutines.flow.SharedFlow 28 | import kotlinx.coroutines.flow.StateFlow 29 | import kotlinx.coroutines.launch 30 | import okhttp3.OkHttpClient 31 | import okhttp3.Request 32 | import okhttp3.WebSocket 33 | import okhttp3.WebSocketListener 34 | 35 | class SignalingClient { 36 | private val logger by taggedLogger("Call:SignalingClient") 37 | private val signalingScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) 38 | private val client = OkHttpClient() 39 | private val request = Request 40 | .Builder() 41 | .url(BuildConfig.SIGNALING_SERVER_IP_ADDRESS) 42 | .build() 43 | 44 | // opening web socket with signaling server 45 | private val ws = client.newWebSocket(request, SignalingWebSocketListener()) 46 | 47 | // session flow to send information about the session state to the subscribers 48 | private val _sessionStateFlow = MutableStateFlow(WebRTCSessionState.Offline) 49 | val sessionStateFlow: StateFlow = _sessionStateFlow 50 | 51 | // signaling commands to send commands to value pairs to the subscribers 52 | private val _signalingCommandFlow = MutableSharedFlow>() 53 | val signalingCommandFlow: SharedFlow> = _signalingCommandFlow 54 | 55 | fun sendCommand(signalingCommand: SignalingCommand, message: String) { 56 | logger.d { "[sendCommand] $signalingCommand $message" } 57 | ws.send("$signalingCommand $message") 58 | } 59 | 60 | private inner class SignalingWebSocketListener : WebSocketListener() { 61 | override fun onMessage(webSocket: WebSocket, text: String) { 62 | when { 63 | text.startsWith(SignalingCommand.STATE.toString(), true) -> 64 | handleStateMessage(text) 65 | text.startsWith(SignalingCommand.OFFER.toString(), true) -> 66 | handleSignalingCommand(SignalingCommand.OFFER, text) 67 | text.startsWith(SignalingCommand.ANSWER.toString(), true) -> 68 | handleSignalingCommand(SignalingCommand.ANSWER, text) 69 | text.startsWith(SignalingCommand.ICE.toString(), true) -> 70 | handleSignalingCommand(SignalingCommand.ICE, text) 71 | } 72 | } 73 | } 74 | 75 | private fun handleStateMessage(message: String) { 76 | val state = getSeparatedMessage(message) 77 | _sessionStateFlow.value = WebRTCSessionState.valueOf(state) 78 | } 79 | 80 | private fun handleSignalingCommand(command: SignalingCommand, text: String) { 81 | val value = getSeparatedMessage(text) 82 | logger.d { "received signaling: $command $value" } 83 | signalingScope.launch { 84 | _signalingCommandFlow.emit(command to value) 85 | } 86 | } 87 | 88 | private fun getSeparatedMessage(text: String) = text.substringAfter(' ') 89 | 90 | fun dispose() { 91 | _sessionStateFlow.value = WebRTCSessionState.Offline 92 | signalingScope.cancel() 93 | ws.cancel() 94 | } 95 | } 96 | 97 | enum class WebRTCSessionState { 98 | Active, // Offer and Answer messages has been sent 99 | Creating, // Creating session, offer has been sent 100 | Ready, // Both clients available and ready to initiate session 101 | Impossible, // We have less than two clients connected to the server 102 | Offline // unable to connect signaling server 103 | } 104 | 105 | enum class SignalingCommand { 106 | STATE, // Command for WebRTCSessionState 107 | OFFER, // to send or receive offer 108 | ANSWER, // to send or receive answer 109 | ICE // to send and receive ice candidates 110 | } 111 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/webrtc/audio/AudioDevice.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.webrtc.audio 18 | 19 | sealed class AudioDevice { 20 | 21 | /** The friendly name of the device.*/ 22 | abstract val name: String 23 | 24 | /** An [AudioDevice] representing a Bluetooth Headset.*/ 25 | data class BluetoothHeadset internal constructor(override val name: String = "Bluetooth") : 26 | AudioDevice() 27 | 28 | /** An [AudioDevice] representing a Wired Headset.*/ 29 | data class WiredHeadset internal constructor(override val name: String = "Wired Headset") : 30 | AudioDevice() 31 | 32 | /** An [AudioDevice] representing the Earpiece.*/ 33 | data class Earpiece internal constructor(override val name: String = "Earpiece") : AudioDevice() 34 | 35 | /** An [AudioDevice] representing the Speakerphone.*/ 36 | data class Speakerphone internal constructor(override val name: String = "Speakerphone") : 37 | AudioDevice() 38 | } 39 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/webrtc/audio/AudioDeviceChangeListener.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.webrtc.audio 18 | 19 | typealias AudioDeviceChangeListener = ( 20 | audioDevices: List, 21 | selectedAudioDevice: AudioDevice? 22 | ) -> Unit 23 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/webrtc/audio/AudioFocusRequestWrapper.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.webrtc.audio 18 | 19 | import android.annotation.SuppressLint 20 | import android.media.AudioAttributes 21 | import android.media.AudioFocusRequest 22 | import android.media.AudioManager 23 | 24 | internal class AudioFocusRequestWrapper { 25 | 26 | @SuppressLint("NewApi") 27 | fun buildRequest(audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener): AudioFocusRequest { 28 | val playbackAttributes = AudioAttributes.Builder() 29 | .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) 30 | .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) 31 | .build() 32 | return AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT) 33 | .setAudioAttributes(playbackAttributes) 34 | .setAcceptsDelayedFocusGain(true) 35 | .setOnAudioFocusChangeListener(audioFocusChangeListener) 36 | .build() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/webrtc/audio/AudioHandler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.webrtc.audio 18 | 19 | import android.content.Context 20 | import android.media.AudioManager 21 | import android.os.Handler 22 | import android.os.Looper 23 | import io.getstream.log.StreamLog 24 | import io.getstream.log.taggedLogger 25 | 26 | interface AudioHandler { 27 | /** 28 | * Called when a room is started. 29 | */ 30 | fun start() 31 | 32 | /** 33 | * Called when a room is disconnected. 34 | */ 35 | fun stop() 36 | } 37 | 38 | class AudioSwitchHandler constructor(private val context: Context) : AudioHandler { 39 | 40 | private val logger by taggedLogger(TAG) 41 | 42 | private var audioDeviceChangeListener: AudioDeviceChangeListener? = null 43 | private var onAudioFocusChangeListener: AudioManager.OnAudioFocusChangeListener? = null 44 | private var preferredDeviceList: List>? = null 45 | 46 | private var audioSwitch: AudioSwitch? = null 47 | 48 | // AudioSwitch is not threadsafe, so all calls should be done on the main thread. 49 | private val handler = Handler(Looper.getMainLooper()) 50 | 51 | override fun start() { 52 | logger.d { "[start] audioSwitch: $audioSwitch" } 53 | if (audioSwitch == null) { 54 | handler.removeCallbacksAndMessages(null) 55 | handler.post { 56 | val switch = AudioSwitch( 57 | context = context, 58 | audioFocusChangeListener = onAudioFocusChangeListener 59 | ?: defaultOnAudioFocusChangeListener, 60 | preferredDeviceList = preferredDeviceList ?: defaultPreferredDeviceList 61 | ) 62 | audioSwitch = switch 63 | switch.start(audioDeviceChangeListener ?: defaultAudioDeviceChangeListener) 64 | switch.activate() 65 | } 66 | } 67 | } 68 | 69 | override fun stop() { 70 | logger.d { "[stop] no args" } 71 | handler.removeCallbacksAndMessages(null) 72 | handler.post { 73 | audioSwitch?.stop() 74 | audioSwitch = null 75 | } 76 | } 77 | 78 | companion object { 79 | private const val TAG = "Call:AudioSwitchHandler" 80 | private val defaultOnAudioFocusChangeListener by lazy(LazyThreadSafetyMode.NONE) { 81 | DefaultOnAudioFocusChangeListener() 82 | } 83 | private val defaultAudioDeviceChangeListener by lazy(LazyThreadSafetyMode.NONE) { 84 | object : AudioDeviceChangeListener { 85 | override fun invoke( 86 | audioDevices: List, 87 | selectedAudioDevice: AudioDevice? 88 | ) { 89 | StreamLog.i(TAG) { "[onAudioDeviceChange] selectedAudioDevice: $selectedAudioDevice" } 90 | } 91 | } 92 | } 93 | private val defaultPreferredDeviceList by lazy(LazyThreadSafetyMode.NONE) { 94 | listOf( 95 | AudioDevice.BluetoothHeadset::class.java, 96 | AudioDevice.WiredHeadset::class.java, 97 | AudioDevice.Earpiece::class.java, 98 | AudioDevice.Speakerphone::class.java 99 | ) 100 | } 101 | 102 | private class DefaultOnAudioFocusChangeListener : AudioManager.OnAudioFocusChangeListener { 103 | override fun onAudioFocusChange(focusChange: Int) { 104 | val typeOfChange: String = when (focusChange) { 105 | AudioManager.AUDIOFOCUS_GAIN -> "AUDIOFOCUS_GAIN" 106 | AudioManager.AUDIOFOCUS_GAIN_TRANSIENT -> "AUDIOFOCUS_GAIN_TRANSIENT" 107 | AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE -> "AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE" 108 | AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK -> "AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK" 109 | AudioManager.AUDIOFOCUS_LOSS -> "AUDIOFOCUS_LOSS" 110 | AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> "AUDIOFOCUS_LOSS_TRANSIENT" 111 | AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK" 112 | else -> "AUDIOFOCUS_INVALID" 113 | } 114 | StreamLog.i(TAG) { "[onAudioFocusChange] focusChange: $typeOfChange" } 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/webrtc/audio/AudioManagerAdapter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.webrtc.audio 18 | 19 | internal interface AudioManagerAdapter { 20 | 21 | fun hasEarpiece(): Boolean 22 | 23 | fun hasSpeakerphone(): Boolean 24 | 25 | fun setAudioFocus() 26 | 27 | fun enableBluetoothSco(enable: Boolean) 28 | 29 | fun enableSpeakerphone(enable: Boolean) 30 | 31 | fun mute(mute: Boolean) 32 | 33 | fun cacheAudioState() 34 | 35 | fun restoreAudioState() 36 | } 37 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/webrtc/audio/AudioManagerAdapterImpl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.webrtc.audio 18 | 19 | import android.annotation.SuppressLint 20 | import android.content.Context 21 | import android.content.pm.PackageManager 22 | import android.media.AudioDeviceInfo 23 | import android.media.AudioFocusRequest 24 | import android.media.AudioManager 25 | import android.os.Build 26 | import io.getstream.log.taggedLogger 27 | 28 | internal class AudioManagerAdapterImpl( 29 | private val context: Context, 30 | private val audioManager: AudioManager, 31 | private val audioFocusRequest: AudioFocusRequestWrapper = AudioFocusRequestWrapper(), 32 | private val audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener 33 | ) : AudioManagerAdapter { 34 | 35 | private val logger by taggedLogger("Call:AudioManager") 36 | 37 | private var savedAudioMode = 0 38 | private var savedIsMicrophoneMuted = false 39 | private var savedSpeakerphoneEnabled = false 40 | private var audioRequest: AudioFocusRequest? = null 41 | 42 | init { 43 | logger.i { " audioFocusChangeListener: $audioFocusChangeListener" } 44 | } 45 | 46 | override fun hasEarpiece(): Boolean { 47 | return context.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY) 48 | } 49 | 50 | @SuppressLint("NewApi") 51 | override fun hasSpeakerphone(): Boolean { 52 | return if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_OUTPUT)) { 53 | val devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS) 54 | for (device in devices) { 55 | if (device.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER) { 56 | return true 57 | } 58 | } 59 | false 60 | } else { 61 | true 62 | } 63 | } 64 | 65 | @SuppressLint("NewApi") 66 | override fun setAudioFocus() { 67 | // Request audio focus before making any device switch. 68 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 69 | audioRequest = audioFocusRequest.buildRequest(audioFocusChangeListener) 70 | audioRequest?.let { 71 | val result = audioManager.requestAudioFocus(it) 72 | logger.i { "[setAudioFocus] #new; completed: ${result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED}" } 73 | } 74 | } else { 75 | val result = audioManager.requestAudioFocus( 76 | audioFocusChangeListener, 77 | AudioManager.STREAM_VOICE_CALL, 78 | AudioManager.AUDIOFOCUS_GAIN_TRANSIENT 79 | ) 80 | logger.i { "[setAudioFocus] #old; completed: ${result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED}" } 81 | } 82 | /* 83 | * Start by setting MODE_IN_COMMUNICATION as default audio mode. It is 84 | * required to be in this mode when playout and/or recording starts for 85 | * best possible VoIP performance. Some devices have difficulties with speaker mode 86 | * if this is not set. 87 | */ 88 | audioManager.mode = AudioManager.MODE_IN_COMMUNICATION 89 | } 90 | 91 | override fun enableBluetoothSco(enable: Boolean) { 92 | logger.i { "[enableBluetoothSco] enable: $enable" } 93 | audioManager.run { if (enable) startBluetoothSco() else stopBluetoothSco() } 94 | } 95 | 96 | override fun enableSpeakerphone(enable: Boolean) { 97 | logger.i { "[enableSpeakerphone] enable: $enable" } 98 | audioManager.isSpeakerphoneOn = enable 99 | } 100 | 101 | override fun mute(mute: Boolean) { 102 | logger.i { "[mute] mute: $mute" } 103 | audioManager.isMicrophoneMute = mute 104 | } 105 | 106 | // TODO Consider persisting audio state in the event of process death 107 | override fun cacheAudioState() { 108 | logger.i { "[cacheAudioState] no args" } 109 | savedAudioMode = audioManager.mode 110 | savedIsMicrophoneMuted = audioManager.isMicrophoneMute 111 | savedSpeakerphoneEnabled = audioManager.isSpeakerphoneOn 112 | } 113 | 114 | @SuppressLint("NewApi") 115 | override fun restoreAudioState() { 116 | logger.i { "[cacheAudioState] no args" } 117 | audioManager.mode = savedAudioMode 118 | mute(savedIsMicrophoneMuted) 119 | enableSpeakerphone(savedSpeakerphoneEnabled) 120 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 121 | audioRequest?.let { 122 | logger.d { "[cacheAudioState] abandonAudioFocusRequest: $it" } 123 | audioManager.abandonAudioFocusRequest(it) 124 | } 125 | } else { 126 | logger.d { "[cacheAudioState] audioFocusChangeListener: $audioFocusChangeListener" } 127 | audioManager.abandonAudioFocus(audioFocusChangeListener) 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/webrtc/audio/AudioSwitch.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.webrtc.audio 18 | 19 | import android.content.Context 20 | import android.media.AudioManager 21 | import io.getstream.log.taggedLogger 22 | import io.getstream.webrtc.sample.compose.webrtc.audio.AudioDevice.BluetoothHeadset 23 | import io.getstream.webrtc.sample.compose.webrtc.audio.AudioDevice.Earpiece 24 | import io.getstream.webrtc.sample.compose.webrtc.audio.AudioDevice.Speakerphone 25 | import io.getstream.webrtc.sample.compose.webrtc.audio.AudioDevice.WiredHeadset 26 | import io.getstream.webrtc.sample.compose.webrtc.audio.AudioSwitch.State.ACTIVATED 27 | import io.getstream.webrtc.sample.compose.webrtc.audio.AudioSwitch.State.STARTED 28 | import io.getstream.webrtc.sample.compose.webrtc.audio.AudioSwitch.State.STOPPED 29 | 30 | class AudioSwitch internal constructor( 31 | context: Context, 32 | audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener, 33 | preferredDeviceList: List>, 34 | private val audioManager: AudioManagerAdapter = AudioManagerAdapterImpl( 35 | context, 36 | context.getSystemService(Context.AUDIO_SERVICE) as AudioManager, 37 | audioFocusChangeListener = audioFocusChangeListener 38 | ) 39 | ) { 40 | 41 | private val logger by taggedLogger("Call:AudioSwitch") 42 | 43 | private var audioDeviceChangeListener: AudioDeviceChangeListener? = null 44 | private var selectedDevice: AudioDevice? = null 45 | private var userSelectedDevice: AudioDevice? = null 46 | private var wiredHeadsetAvailable = false 47 | private val mutableAudioDevices = ArrayList() 48 | private val preferredDeviceList: List> 49 | 50 | private var state: State = STOPPED 51 | 52 | internal enum class State { 53 | STARTED, ACTIVATED, STOPPED 54 | } 55 | 56 | init { 57 | this.preferredDeviceList = getPreferredDeviceList(preferredDeviceList) 58 | } 59 | 60 | private fun getPreferredDeviceList(preferredDeviceList: List>): 61 | List> { 62 | require(hasNoDuplicates(preferredDeviceList)) 63 | 64 | return if (preferredDeviceList.isEmpty() || preferredDeviceList == defaultPreferredDeviceList) { 65 | defaultPreferredDeviceList 66 | } else { 67 | val result = defaultPreferredDeviceList.toMutableList() 68 | result.removeAll(preferredDeviceList) 69 | preferredDeviceList.forEachIndexed { index, device -> 70 | result.add(index, device) 71 | } 72 | result 73 | } 74 | } 75 | 76 | /** 77 | * Starts listening for audio device changes and calls the [listener] upon each change. 78 | * **Note:** When audio device listening is no longer needed, [AudioSwitch.stop] should be 79 | * called in order to prevent a memory leak. 80 | */ 81 | fun start(listener: AudioDeviceChangeListener) { 82 | logger.d { "[start] state: $state" } 83 | audioDeviceChangeListener = listener 84 | when (state) { 85 | STOPPED -> { 86 | enumerateDevices() 87 | state = STARTED 88 | } 89 | else -> { 90 | } 91 | } 92 | } 93 | 94 | /** 95 | * Stops listening for audio device changes if [AudioSwitch.start] has already been 96 | * invoked. [AudioSwitch.deactivate] will also get called if a device has been activated 97 | * with [AudioSwitch.activate]. 98 | */ 99 | fun stop() { 100 | logger.d { "[stop] state: $state" } 101 | when (state) { 102 | ACTIVATED -> { 103 | deactivate() 104 | closeListeners() 105 | } 106 | STARTED -> { 107 | closeListeners() 108 | } 109 | STOPPED -> { 110 | } 111 | } 112 | } 113 | 114 | /** 115 | * Performs audio routing and unmuting on the selected device from 116 | * [AudioSwitch.selectDevice]. Audio focus is also acquired for the client application. 117 | * **Note:** [AudioSwitch.deactivate] should be invoked to restore the prior audio 118 | * state. 119 | */ 120 | fun activate() { 121 | logger.d { "[activate] state: $state" } 122 | when (state) { 123 | STARTED -> { 124 | audioManager.cacheAudioState() 125 | 126 | // Always set mute to false for WebRTC 127 | audioManager.mute(false) 128 | audioManager.setAudioFocus() 129 | selectedDevice?.let { activate(it) } 130 | state = ACTIVATED 131 | } 132 | ACTIVATED -> selectedDevice?.let { activate(it) } 133 | STOPPED -> throw IllegalStateException() 134 | } 135 | } 136 | 137 | /** 138 | * Restores the audio state prior to calling [AudioSwitch.activate] and removes 139 | * audio focus from the client application. 140 | */ 141 | private fun deactivate() { 142 | logger.d { "[deactivate] state: $state" } 143 | when (state) { 144 | ACTIVATED -> { 145 | // Restore stored audio state 146 | audioManager.restoreAudioState() 147 | state = STARTED 148 | } 149 | STARTED, STOPPED -> { 150 | } 151 | } 152 | } 153 | 154 | /** 155 | * Selects the desired [audioDevice]. If the provided [AudioDevice] is not 156 | * available, no changes are made. If the provided device is null, one is chosen based on the 157 | * specified preferred device list or the following default list: 158 | * [BluetoothHeadset], [WiredHeadset], [Earpiece], [Speakerphone]. 159 | */ 160 | private fun selectDevice(audioDevice: AudioDevice?) { 161 | logger.d { "[selectDevice] audioDevice: $audioDevice" } 162 | if (selectedDevice != audioDevice) { 163 | userSelectedDevice = audioDevice 164 | enumerateDevices() 165 | } 166 | } 167 | 168 | private fun hasNoDuplicates(list: List>) = 169 | list.groupingBy { it }.eachCount().filter { it.value > 1 }.isEmpty() 170 | 171 | private fun activate(audioDevice: AudioDevice) { 172 | logger.d { "[activate] audioDevice: $audioDevice" } 173 | when (audioDevice) { 174 | is BluetoothHeadset -> audioManager.enableSpeakerphone(false) 175 | is Earpiece, is WiredHeadset -> audioManager.enableSpeakerphone(false) 176 | is Speakerphone -> audioManager.enableSpeakerphone(true) 177 | } 178 | } 179 | 180 | internal data class AudioDeviceState( 181 | val audioDeviceList: List, 182 | val selectedAudioDevice: AudioDevice? 183 | ) 184 | 185 | private fun enumerateDevices(bluetoothHeadsetName: String? = null) { 186 | logger.d { "[enumerateDevices] bluetoothHeadsetName: $bluetoothHeadsetName" } 187 | // save off the old state and 'semi'-deep copy the list of audio devices 188 | val oldAudioDeviceState = AudioDeviceState(mutableAudioDevices.map { it }, selectedDevice) 189 | // update audio device list and selected device 190 | addAvailableAudioDevices(bluetoothHeadsetName) 191 | 192 | if (!userSelectedDevicePresent(mutableAudioDevices)) { 193 | userSelectedDevice = null 194 | } 195 | 196 | // Select the audio device 197 | selectedDevice = if (userSelectedDevice != null) { 198 | userSelectedDevice 199 | } else if (mutableAudioDevices.size > 0) { 200 | mutableAudioDevices.first() 201 | } else { 202 | null 203 | } 204 | logger.v { "[enumerateDevices] selectedDevice: $selectedDevice" } 205 | 206 | // Activate the device if in the active state 207 | if (state == ACTIVATED) { 208 | activate() 209 | } 210 | // trigger audio device change listener if there has been a change 211 | val newAudioDeviceState = AudioDeviceState(mutableAudioDevices, selectedDevice) 212 | if (newAudioDeviceState != oldAudioDeviceState) { 213 | audioDeviceChangeListener?.invoke(mutableAudioDevices, selectedDevice) 214 | } 215 | } 216 | 217 | private fun addAvailableAudioDevices(bluetoothHeadsetName: String?) { 218 | logger.d { 219 | "[addAvailableAudioDevices] wiredHeadsetAvailable: $wiredHeadsetAvailable, " + 220 | "bluetoothHeadsetName: $bluetoothHeadsetName" 221 | } 222 | mutableAudioDevices.clear() 223 | preferredDeviceList.forEach { audioDevice -> 224 | logger.v { "[addAvailableAudioDevices] audioDevice: ${audioDevice.simpleName}" } 225 | when (audioDevice) { 226 | BluetoothHeadset::class.java -> { 227 | /* 228 | * Since the there is a delay between receiving the ACTION_ACL_CONNECTED event and receiving 229 | * the name of the connected device from querying the BluetoothHeadset proxy class, the 230 | * headset name received from the ACTION_ACL_CONNECTED intent needs to be passed into this 231 | * function. 232 | */ 233 | } 234 | WiredHeadset::class.java -> { 235 | logger.v { 236 | "[addAvailableAudioDevices] #WiredHeadset; wiredHeadsetAvailable: $wiredHeadsetAvailable" 237 | } 238 | if (wiredHeadsetAvailable) { 239 | mutableAudioDevices.add(WiredHeadset()) 240 | } 241 | } 242 | Earpiece::class.java -> { 243 | val hasEarpiece = audioManager.hasEarpiece() 244 | logger.v { 245 | "[addAvailableAudioDevices] #Earpiece; hasEarpiece: $hasEarpiece, " + 246 | "wiredHeadsetAvailable: $wiredHeadsetAvailable" 247 | } 248 | if (hasEarpiece && !wiredHeadsetAvailable) { 249 | mutableAudioDevices.add(Earpiece()) 250 | } 251 | } 252 | Speakerphone::class.java -> { 253 | val hasSpeakerphone = audioManager.hasSpeakerphone() 254 | logger.v { "[addAvailableAudioDevices] #Speakerphone; hasSpeakerphone: $hasSpeakerphone" } 255 | if (hasSpeakerphone) { 256 | mutableAudioDevices.add(Speakerphone()) 257 | } 258 | } 259 | } 260 | } 261 | } 262 | 263 | private fun userSelectedDevicePresent(audioDevices: List) = 264 | userSelectedDevice?.let { selectedDevice -> 265 | if (selectedDevice is BluetoothHeadset) { 266 | // Match any bluetooth headset as a new one may have been connected 267 | audioDevices.find { it is BluetoothHeadset }?.let { newHeadset -> 268 | userSelectedDevice = newHeadset 269 | true 270 | } ?: false 271 | } else { 272 | audioDevices.contains(selectedDevice) 273 | } 274 | } ?: false 275 | 276 | private fun closeListeners() { 277 | audioDeviceChangeListener = null 278 | state = STOPPED 279 | } 280 | 281 | companion object { 282 | private val defaultPreferredDeviceList by lazy { 283 | listOf( 284 | BluetoothHeadset::class.java, 285 | WiredHeadset::class.java, 286 | Earpiece::class.java, 287 | Speakerphone::class.java 288 | ) 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/webrtc/peer/StreamPeerConnectionFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.webrtc.peer 18 | 19 | import android.content.Context 20 | import android.os.Build 21 | import io.getstream.log.taggedLogger 22 | import kotlinx.coroutines.CoroutineScope 23 | import org.webrtc.AudioSource 24 | import org.webrtc.AudioTrack 25 | import org.webrtc.DefaultVideoDecoderFactory 26 | import org.webrtc.EglBase 27 | import org.webrtc.HardwareVideoEncoderFactory 28 | import org.webrtc.IceCandidate 29 | import org.webrtc.Logging 30 | import org.webrtc.MediaConstraints 31 | import org.webrtc.MediaStream 32 | import org.webrtc.PeerConnection 33 | import org.webrtc.PeerConnectionFactory 34 | import org.webrtc.RtpTransceiver 35 | import org.webrtc.SimulcastVideoEncoderFactory 36 | import org.webrtc.SoftwareVideoEncoderFactory 37 | import org.webrtc.VideoSource 38 | import org.webrtc.VideoTrack 39 | import org.webrtc.audio.JavaAudioDeviceModule 40 | 41 | class StreamPeerConnectionFactory constructor( 42 | private val context: Context 43 | ) { 44 | private val webRtcLogger by taggedLogger("Call:WebRTC") 45 | private val audioLogger by taggedLogger("Call:AudioTrackCallback") 46 | 47 | val eglBaseContext: EglBase.Context by lazy { 48 | EglBase.create().eglBaseContext 49 | } 50 | 51 | /** 52 | * Default video decoder factory used to unpack video from the remote tracks. 53 | */ 54 | private val videoDecoderFactory by lazy { 55 | DefaultVideoDecoderFactory( 56 | eglBaseContext 57 | ) 58 | } 59 | 60 | // rtcConfig contains STUN and TURN servers list 61 | val rtcConfig = PeerConnection.RTCConfiguration( 62 | arrayListOf( 63 | // adding google's standard server 64 | PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer() 65 | ) 66 | ).apply { 67 | // it's very important to use new unified sdp semantics PLAN_B is deprecated 68 | sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN 69 | } 70 | 71 | /** 72 | * Default encoder factory that supports Simulcast, used to send video tracks to the server. 73 | */ 74 | private val videoEncoderFactory by lazy { 75 | val hardwareEncoder = HardwareVideoEncoderFactory(eglBaseContext, true, true) 76 | SimulcastVideoEncoderFactory(hardwareEncoder, SoftwareVideoEncoderFactory()) 77 | } 78 | 79 | /** 80 | * Factory that builds all the connections based on the extensive configuration provided under 81 | * the hood. 82 | */ 83 | private val factory by lazy { 84 | PeerConnectionFactory.initialize( 85 | PeerConnectionFactory.InitializationOptions.builder(context) 86 | .setInjectableLogger({ message, severity, label -> 87 | when (severity) { 88 | Logging.Severity.LS_VERBOSE -> { 89 | webRtcLogger.v { "[onLogMessage] label: $label, message: $message" } 90 | } 91 | Logging.Severity.LS_INFO -> { 92 | webRtcLogger.i { "[onLogMessage] label: $label, message: $message" } 93 | } 94 | Logging.Severity.LS_WARNING -> { 95 | webRtcLogger.w { "[onLogMessage] label: $label, message: $message" } 96 | } 97 | Logging.Severity.LS_ERROR -> { 98 | webRtcLogger.e { "[onLogMessage] label: $label, message: $message" } 99 | } 100 | Logging.Severity.LS_NONE -> { 101 | webRtcLogger.d { "[onLogMessage] label: $label, message: $message" } 102 | } 103 | else -> {} 104 | } 105 | }, Logging.Severity.LS_VERBOSE) 106 | .createInitializationOptions() 107 | ) 108 | 109 | PeerConnectionFactory.builder() 110 | .setVideoDecoderFactory(videoDecoderFactory) 111 | .setVideoEncoderFactory(videoEncoderFactory) 112 | .setAudioDeviceModule( 113 | JavaAudioDeviceModule 114 | .builder(context) 115 | .setUseHardwareAcousticEchoCanceler(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) 116 | .setUseHardwareNoiseSuppressor(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) 117 | .setAudioRecordErrorCallback(object : 118 | JavaAudioDeviceModule.AudioRecordErrorCallback { 119 | override fun onWebRtcAudioRecordInitError(p0: String?) { 120 | audioLogger.w { "[onWebRtcAudioRecordInitError] $p0" } 121 | } 122 | 123 | override fun onWebRtcAudioRecordStartError( 124 | p0: JavaAudioDeviceModule.AudioRecordStartErrorCode?, 125 | p1: String? 126 | ) { 127 | audioLogger.w { "[onWebRtcAudioRecordInitError] $p1" } 128 | } 129 | 130 | override fun onWebRtcAudioRecordError(p0: String?) { 131 | audioLogger.w { "[onWebRtcAudioRecordError] $p0" } 132 | } 133 | }) 134 | .setAudioTrackErrorCallback(object : 135 | JavaAudioDeviceModule.AudioTrackErrorCallback { 136 | override fun onWebRtcAudioTrackInitError(p0: String?) { 137 | audioLogger.w { "[onWebRtcAudioTrackInitError] $p0" } 138 | } 139 | 140 | override fun onWebRtcAudioTrackStartError( 141 | p0: JavaAudioDeviceModule.AudioTrackStartErrorCode?, 142 | p1: String? 143 | ) { 144 | audioLogger.w { "[onWebRtcAudioTrackStartError] $p0" } 145 | } 146 | 147 | override fun onWebRtcAudioTrackError(p0: String?) { 148 | audioLogger.w { "[onWebRtcAudioTrackError] $p0" } 149 | } 150 | }) 151 | .setAudioRecordStateCallback(object : 152 | JavaAudioDeviceModule.AudioRecordStateCallback { 153 | override fun onWebRtcAudioRecordStart() { 154 | audioLogger.d { "[onWebRtcAudioRecordStart] no args" } 155 | } 156 | 157 | override fun onWebRtcAudioRecordStop() { 158 | audioLogger.d { "[onWebRtcAudioRecordStop] no args" } 159 | } 160 | }) 161 | .setAudioTrackStateCallback(object : 162 | JavaAudioDeviceModule.AudioTrackStateCallback { 163 | override fun onWebRtcAudioTrackStart() { 164 | audioLogger.d { "[onWebRtcAudioTrackStart] no args" } 165 | } 166 | 167 | override fun onWebRtcAudioTrackStop() { 168 | audioLogger.d { "[onWebRtcAudioTrackStop] no args" } 169 | } 170 | }) 171 | .createAudioDeviceModule().also { 172 | it.setMicrophoneMute(false) 173 | it.setSpeakerMute(false) 174 | } 175 | ) 176 | .createPeerConnectionFactory() 177 | } 178 | 179 | /** 180 | * Builds a [StreamPeerConnection] that wraps the WebRTC [PeerConnection] and exposes several 181 | * helpful handlers. 182 | * 183 | * @param coroutineScope Scope used for asynchronous operations. 184 | * @param configuration The [PeerConnection.RTCConfiguration] used to set up the connection. 185 | * @param type The type of connection, either a subscriber of a publisher. 186 | * @param mediaConstraints Constraints used for audio and video tracks in the connection. 187 | * @param onStreamAdded Handler when a new [MediaStream] gets added. 188 | * @param onNegotiationNeeded Handler when there's a new negotiation. 189 | * @param onIceCandidateRequest Handler whenever we receive [IceCandidate]s. 190 | * @return [StreamPeerConnection] That's fully set up and can be observed and used to send and 191 | * receive tracks. 192 | */ 193 | fun makePeerConnection( 194 | coroutineScope: CoroutineScope, 195 | configuration: PeerConnection.RTCConfiguration, 196 | type: StreamPeerType, 197 | mediaConstraints: MediaConstraints, 198 | onStreamAdded: ((MediaStream) -> Unit)? = null, 199 | onNegotiationNeeded: ((StreamPeerConnection, StreamPeerType) -> Unit)? = null, 200 | onIceCandidateRequest: ((IceCandidate, StreamPeerType) -> Unit)? = null, 201 | onVideoTrack: ((RtpTransceiver?) -> Unit)? = null 202 | ): StreamPeerConnection { 203 | val peerConnection = StreamPeerConnection( 204 | coroutineScope = coroutineScope, 205 | type = type, 206 | mediaConstraints = mediaConstraints, 207 | onStreamAdded = onStreamAdded, 208 | onNegotiationNeeded = onNegotiationNeeded, 209 | onIceCandidate = onIceCandidateRequest, 210 | onVideoTrack = onVideoTrack 211 | ) 212 | val connection = makePeerConnectionInternal( 213 | configuration = configuration, 214 | observer = peerConnection 215 | ) 216 | return peerConnection.apply { initialize(connection) } 217 | } 218 | 219 | /** 220 | * Builds a [PeerConnection] internally that connects to the server and is able to send and 221 | * receive tracks. 222 | * 223 | * @param configuration The [PeerConnection.RTCConfiguration] used to set up the connection. 224 | * @param observer Handler used to observe different states of the connection. 225 | * @return [PeerConnection] that's fully set up. 226 | */ 227 | private fun makePeerConnectionInternal( 228 | configuration: PeerConnection.RTCConfiguration, 229 | observer: PeerConnection.Observer? 230 | ): PeerConnection { 231 | return requireNotNull( 232 | factory.createPeerConnection( 233 | configuration, 234 | observer 235 | ) 236 | ) 237 | } 238 | 239 | /** 240 | * Builds a [VideoSource] from the [factory] that can be used for regular video share (camera) 241 | * or screen sharing. 242 | * 243 | * @param isScreencast If we're screen sharing using this source. 244 | * @return [VideoSource] that can be used to build tracks. 245 | */ 246 | fun makeVideoSource(isScreencast: Boolean): VideoSource = 247 | factory.createVideoSource(isScreencast) 248 | 249 | /** 250 | * Builds a [VideoTrack] from the [factory] that can be used for regular video share (camera) 251 | * or screen sharing. 252 | * 253 | * @param source The [VideoSource] used for the track. 254 | * @param trackId The unique ID for this track. 255 | * @return [VideoTrack] That represents a video feed. 256 | */ 257 | fun makeVideoTrack( 258 | source: VideoSource, 259 | trackId: String 260 | ): VideoTrack = factory.createVideoTrack(trackId, source) 261 | 262 | /** 263 | * Builds an [AudioSource] from the [factory] that can be used for audio sharing. 264 | * 265 | * @param constraints The constraints used to change the way the audio behaves. 266 | * @return [AudioSource] that can be used to build tracks. 267 | */ 268 | fun makeAudioSource(constraints: MediaConstraints = MediaConstraints()): AudioSource = 269 | factory.createAudioSource(constraints) 270 | 271 | /** 272 | * Builds an [AudioTrack] from the [factory] that can be used for regular video share (camera) 273 | * or screen sharing. 274 | * 275 | * @param source The [AudioSource] used for the track. 276 | * @param trackId The unique ID for this track. 277 | * @return [AudioTrack] That represents an audio feed. 278 | */ 279 | fun makeAudioTrack( 280 | source: AudioSource, 281 | trackId: String 282 | ): AudioTrack = factory.createAudioTrack(trackId, source) 283 | } 284 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/webrtc/peer/StreamPeerType.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.webrtc.peer 18 | 19 | /** 20 | * The type of peer connections, either a [PUBLISHER] that sends data to the call or a [SUBSCRIBER] 21 | * that receives and decodes the data from the server. 22 | */ 23 | enum class StreamPeerType { 24 | PUBLISHER, 25 | SUBSCRIBER 26 | } 27 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/webrtc/sessions/WebRtcSessionManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.webrtc.sessions 18 | 19 | import io.getstream.webrtc.sample.compose.webrtc.SignalingClient 20 | import io.getstream.webrtc.sample.compose.webrtc.peer.StreamPeerConnectionFactory 21 | import kotlinx.coroutines.flow.SharedFlow 22 | import org.webrtc.VideoTrack 23 | 24 | interface WebRtcSessionManager { 25 | 26 | val signalingClient: SignalingClient 27 | 28 | val peerConnectionFactory: StreamPeerConnectionFactory 29 | 30 | val localVideoTrackFlow: SharedFlow 31 | 32 | val remoteVideoTrackFlow: SharedFlow 33 | 34 | fun onSessionScreenReady() 35 | 36 | fun flipCamera() 37 | 38 | fun enableMicrophone(enabled: Boolean) 39 | 40 | fun enableCamera(enabled: Boolean) 41 | 42 | fun disconnect() 43 | } 44 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/webrtc/sessions/WebRtcSessionManagerImpl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.webrtc.sessions 18 | 19 | import android.content.Context 20 | import android.hardware.camera2.CameraCharacteristics 21 | import android.hardware.camera2.CameraManager 22 | import android.hardware.camera2.CameraMetadata 23 | import android.media.AudioDeviceInfo 24 | import android.media.AudioManager 25 | import android.os.Build 26 | import androidx.compose.runtime.ProvidableCompositionLocal 27 | import androidx.compose.runtime.staticCompositionLocalOf 28 | import androidx.core.content.getSystemService 29 | import io.getstream.log.taggedLogger 30 | import io.getstream.webrtc.sample.compose.webrtc.SignalingClient 31 | import io.getstream.webrtc.sample.compose.webrtc.SignalingCommand 32 | import io.getstream.webrtc.sample.compose.webrtc.audio.AudioHandler 33 | import io.getstream.webrtc.sample.compose.webrtc.audio.AudioSwitchHandler 34 | import io.getstream.webrtc.sample.compose.webrtc.peer.StreamPeerConnection 35 | import io.getstream.webrtc.sample.compose.webrtc.peer.StreamPeerConnectionFactory 36 | import io.getstream.webrtc.sample.compose.webrtc.peer.StreamPeerType 37 | import io.getstream.webrtc.sample.compose.webrtc.utils.stringify 38 | import kotlinx.coroutines.CoroutineScope 39 | import kotlinx.coroutines.Dispatchers 40 | import kotlinx.coroutines.SupervisorJob 41 | import kotlinx.coroutines.flow.MutableSharedFlow 42 | import kotlinx.coroutines.flow.SharedFlow 43 | import kotlinx.coroutines.launch 44 | import org.webrtc.AudioTrack 45 | import org.webrtc.Camera2Capturer 46 | import org.webrtc.Camera2Enumerator 47 | import org.webrtc.CameraEnumerationAndroid 48 | import org.webrtc.IceCandidate 49 | import org.webrtc.MediaConstraints 50 | import org.webrtc.MediaStreamTrack 51 | import org.webrtc.SessionDescription 52 | import org.webrtc.SurfaceTextureHelper 53 | import org.webrtc.VideoCapturer 54 | import org.webrtc.VideoTrack 55 | import java.util.UUID 56 | 57 | private const val ICE_SEPARATOR = '$' 58 | 59 | val LocalWebRtcSessionManager: ProvidableCompositionLocal = 60 | staticCompositionLocalOf { error("WebRtcSessionManager was not initialized!") } 61 | 62 | class WebRtcSessionManagerImpl( 63 | private val context: Context, 64 | override val signalingClient: SignalingClient, 65 | override val peerConnectionFactory: StreamPeerConnectionFactory 66 | ) : WebRtcSessionManager { 67 | private val logger by taggedLogger("Call:LocalWebRtcSessionManager") 68 | 69 | private val sessionManagerScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) 70 | 71 | // used to send local video track to the fragment 72 | private val _localVideoTrackFlow = MutableSharedFlow() 73 | override val localVideoTrackFlow: SharedFlow = _localVideoTrackFlow 74 | 75 | // used to send remote video track to the sender 76 | private val _remoteVideoTrackFlow = MutableSharedFlow() 77 | override val remoteVideoTrackFlow: SharedFlow = _remoteVideoTrackFlow 78 | 79 | // declaring video constraints and setting OfferToReceiveVideo to true 80 | // this step is mandatory to create valid offer and answer 81 | private val mediaConstraints = MediaConstraints().apply { 82 | mandatory.addAll( 83 | listOf( 84 | MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"), 85 | MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true") 86 | ) 87 | ) 88 | } 89 | 90 | // getting front camera 91 | private val videoCapturer: VideoCapturer by lazy { buildCameraCapturer() } 92 | private val cameraManager by lazy { context.getSystemService() } 93 | private val cameraEnumerator: Camera2Enumerator by lazy { 94 | Camera2Enumerator(context) 95 | } 96 | 97 | private val resolution: CameraEnumerationAndroid.CaptureFormat 98 | get() { 99 | val frontCamera = cameraEnumerator.deviceNames.first { cameraName -> 100 | cameraEnumerator.isFrontFacing(cameraName) 101 | } 102 | val supportedFormats = cameraEnumerator.getSupportedFormats(frontCamera) ?: emptyList() 103 | return supportedFormats.firstOrNull { 104 | (it.width == 720 || it.width == 480 || it.width == 360) 105 | } ?: error("There is no matched resolution!") 106 | } 107 | 108 | // we need it to initialize video capturer 109 | private val surfaceTextureHelper = SurfaceTextureHelper.create( 110 | "SurfaceTextureHelperThread", 111 | peerConnectionFactory.eglBaseContext 112 | ) 113 | 114 | private val videoSource by lazy { 115 | peerConnectionFactory.makeVideoSource(videoCapturer.isScreencast).apply { 116 | videoCapturer.initialize(surfaceTextureHelper, context, this.capturerObserver) 117 | videoCapturer.startCapture(resolution.width, resolution.height, 30) 118 | } 119 | } 120 | 121 | private val localVideoTrack: VideoTrack by lazy { 122 | peerConnectionFactory.makeVideoTrack( 123 | source = videoSource, 124 | trackId = "Video${UUID.randomUUID()}" 125 | ) 126 | } 127 | 128 | /** Audio properties */ 129 | 130 | private val audioHandler: AudioHandler by lazy { 131 | AudioSwitchHandler(context) 132 | } 133 | 134 | private val audioManager by lazy { 135 | context.getSystemService() 136 | } 137 | 138 | private val audioConstraints: MediaConstraints by lazy { 139 | buildAudioConstraints() 140 | } 141 | 142 | private val audioSource by lazy { 143 | peerConnectionFactory.makeAudioSource(audioConstraints) 144 | } 145 | 146 | private val localAudioTrack: AudioTrack by lazy { 147 | peerConnectionFactory.makeAudioTrack( 148 | source = audioSource, 149 | trackId = "Audio${UUID.randomUUID()}" 150 | ) 151 | } 152 | 153 | private var offer: String? = null 154 | 155 | private val peerConnection: StreamPeerConnection by lazy { 156 | peerConnectionFactory.makePeerConnection( 157 | coroutineScope = sessionManagerScope, 158 | configuration = peerConnectionFactory.rtcConfig, 159 | type = StreamPeerType.SUBSCRIBER, 160 | mediaConstraints = mediaConstraints, 161 | onIceCandidateRequest = { iceCandidate, _ -> 162 | signalingClient.sendCommand( 163 | SignalingCommand.ICE, 164 | "${iceCandidate.sdpMid}$ICE_SEPARATOR${iceCandidate.sdpMLineIndex}$ICE_SEPARATOR${iceCandidate.sdp}" 165 | ) 166 | }, 167 | onVideoTrack = { rtpTransceiver -> 168 | val track = rtpTransceiver?.receiver?.track() ?: return@makePeerConnection 169 | if (track.kind() == MediaStreamTrack.VIDEO_TRACK_KIND) { 170 | val videoTrack = track as VideoTrack 171 | sessionManagerScope.launch { 172 | _remoteVideoTrackFlow.emit(videoTrack) 173 | } 174 | } 175 | } 176 | ) 177 | } 178 | 179 | init { 180 | sessionManagerScope.launch { 181 | signalingClient.signalingCommandFlow 182 | .collect { commandToValue -> 183 | when (commandToValue.first) { 184 | SignalingCommand.OFFER -> handleOffer(commandToValue.second) 185 | SignalingCommand.ANSWER -> handleAnswer(commandToValue.second) 186 | SignalingCommand.ICE -> handleIce(commandToValue.second) 187 | else -> Unit 188 | } 189 | } 190 | } 191 | } 192 | 193 | override fun onSessionScreenReady() { 194 | setupAudio() 195 | peerConnection.connection.addTrack(localVideoTrack) 196 | peerConnection.connection.addTrack(localAudioTrack) 197 | sessionManagerScope.launch { 198 | // sending local video track to show local video from start 199 | _localVideoTrackFlow.emit(localVideoTrack) 200 | 201 | if (offer != null) { 202 | sendAnswer() 203 | } else { 204 | sendOffer() 205 | } 206 | } 207 | } 208 | 209 | override fun flipCamera() { 210 | (videoCapturer as? Camera2Capturer)?.switchCamera(null) 211 | } 212 | 213 | override fun enableMicrophone(enabled: Boolean) { 214 | audioManager?.isMicrophoneMute = !enabled 215 | } 216 | 217 | override fun enableCamera(enabled: Boolean) { 218 | if (enabled) { 219 | videoCapturer.startCapture(resolution.width, resolution.height, 30) 220 | } else { 221 | videoCapturer.stopCapture() 222 | } 223 | } 224 | 225 | override fun disconnect() { 226 | // dispose audio & video tracks. 227 | remoteVideoTrackFlow.replayCache.forEach { videoTrack -> 228 | videoTrack.dispose() 229 | } 230 | localVideoTrackFlow.replayCache.forEach { videoTrack -> 231 | videoTrack.dispose() 232 | } 233 | localAudioTrack.dispose() 234 | localVideoTrack.dispose() 235 | 236 | // dispose audio handler and video capturer. 237 | audioHandler.stop() 238 | videoCapturer.stopCapture() 239 | videoCapturer.dispose() 240 | 241 | // dispose signaling clients and socket. 242 | signalingClient.dispose() 243 | } 244 | 245 | private suspend fun sendOffer() { 246 | val offer = peerConnection.createOffer().getOrThrow() 247 | val result = peerConnection.setLocalDescription(offer) 248 | result.onSuccess { 249 | signalingClient.sendCommand(SignalingCommand.OFFER, offer.description) 250 | } 251 | logger.d { "[SDP] send offer: ${offer.stringify()}" } 252 | } 253 | 254 | private suspend fun sendAnswer() { 255 | peerConnection.setRemoteDescription( 256 | SessionDescription(SessionDescription.Type.OFFER, offer) 257 | ) 258 | val answer = peerConnection.createAnswer().getOrThrow() 259 | val result = peerConnection.setLocalDescription(answer) 260 | result.onSuccess { 261 | signalingClient.sendCommand(SignalingCommand.ANSWER, answer.description) 262 | } 263 | logger.d { "[SDP] send answer: ${answer.stringify()}" } 264 | } 265 | 266 | private fun handleOffer(sdp: String) { 267 | logger.d { "[SDP] handle offer: $sdp" } 268 | offer = sdp 269 | } 270 | 271 | private suspend fun handleAnswer(sdp: String) { 272 | logger.d { "[SDP] handle answer: $sdp" } 273 | peerConnection.setRemoteDescription( 274 | SessionDescription(SessionDescription.Type.ANSWER, sdp) 275 | ) 276 | } 277 | 278 | private suspend fun handleIce(iceMessage: String) { 279 | val iceArray = iceMessage.split(ICE_SEPARATOR) 280 | peerConnection.addIceCandidate( 281 | IceCandidate( 282 | iceArray[0], 283 | iceArray[1].toInt(), 284 | iceArray[2] 285 | ) 286 | ) 287 | } 288 | 289 | private fun buildCameraCapturer(): VideoCapturer { 290 | val manager = cameraManager ?: throw RuntimeException("CameraManager was not initialized!") 291 | 292 | val ids = manager.cameraIdList 293 | var foundCamera = false 294 | var cameraId = "" 295 | 296 | for (id in ids) { 297 | val characteristics = manager.getCameraCharacteristics(id) 298 | val cameraLensFacing = characteristics.get(CameraCharacteristics.LENS_FACING) 299 | 300 | if (cameraLensFacing == CameraMetadata.LENS_FACING_FRONT) { 301 | foundCamera = true 302 | cameraId = id 303 | } 304 | } 305 | 306 | if (!foundCamera && ids.isNotEmpty()) { 307 | cameraId = ids.first() 308 | } 309 | 310 | val camera2Capturer = Camera2Capturer(context, cameraId, null) 311 | return camera2Capturer 312 | } 313 | 314 | private fun buildAudioConstraints(): MediaConstraints { 315 | val mediaConstraints = MediaConstraints() 316 | val items = listOf( 317 | MediaConstraints.KeyValuePair( 318 | "googEchoCancellation", 319 | true.toString() 320 | ), 321 | MediaConstraints.KeyValuePair( 322 | "googAutoGainControl", 323 | true.toString() 324 | ), 325 | MediaConstraints.KeyValuePair( 326 | "googHighpassFilter", 327 | true.toString() 328 | ), 329 | MediaConstraints.KeyValuePair( 330 | "googNoiseSuppression", 331 | true.toString() 332 | ), 333 | MediaConstraints.KeyValuePair( 334 | "googTypingNoiseDetection", 335 | true.toString() 336 | ) 337 | ) 338 | 339 | return mediaConstraints.apply { 340 | with(optional) { 341 | add(MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true")) 342 | addAll(items) 343 | } 344 | } 345 | } 346 | 347 | private fun setupAudio() { 348 | logger.d { "[setupAudio] #sfu; no args" } 349 | audioHandler.start() 350 | audioManager?.mode = AudioManager.MODE_IN_COMMUNICATION 351 | 352 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 353 | val devices = audioManager?.availableCommunicationDevices ?: return 354 | val deviceType = AudioDeviceInfo.TYPE_BUILTIN_SPEAKER 355 | 356 | val device = devices.firstOrNull { it.type == deviceType } ?: return 357 | 358 | val isCommunicationDeviceSet = audioManager?.setCommunicationDevice(device) 359 | logger.d { "[setupAudio] #sfu; isCommunicationDeviceSet: $isCommunicationDeviceSet" } 360 | } 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/webrtc/utils/PeerConnectionUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.webrtc.utils 18 | 19 | import org.webrtc.AddIceObserver 20 | import org.webrtc.IceCandidate 21 | import org.webrtc.PeerConnection 22 | import kotlin.coroutines.resume 23 | import kotlin.coroutines.suspendCoroutine 24 | 25 | suspend fun PeerConnection.addRtcIceCandidate(iceCandidate: IceCandidate): Result { 26 | return suspendCoroutine { cont -> 27 | addIceCandidate( 28 | iceCandidate, 29 | object : AddIceObserver { 30 | override fun onAddSuccess() { 31 | cont.resume(Result.success(Unit)) 32 | } 33 | 34 | override fun onAddFailure(error: String?) { 35 | cont.resume(Result.failure(RuntimeException(error))) 36 | } 37 | } 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/webrtc/utils/SDPUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.webrtc.utils 18 | 19 | import org.webrtc.SdpObserver 20 | import org.webrtc.SessionDescription 21 | import kotlin.coroutines.resume 22 | import kotlin.coroutines.suspendCoroutine 23 | 24 | suspend inline fun createValue( 25 | crossinline call: (SdpObserver) -> Unit 26 | ): Result = suspendCoroutine { 27 | val observer = object : SdpObserver { 28 | 29 | /** 30 | * Handling of create values. 31 | */ 32 | override fun onCreateSuccess(description: SessionDescription?) { 33 | if (description != null) { 34 | it.resume(Result.success(description)) 35 | } else { 36 | it.resume(Result.failure(RuntimeException("SessionDescription is null!"))) 37 | } 38 | } 39 | 40 | override fun onCreateFailure(message: String?) = 41 | it.resume(Result.failure(RuntimeException(message))) 42 | 43 | /** 44 | * We ignore set results. 45 | */ 46 | override fun onSetSuccess() = Unit 47 | override fun onSetFailure(p0: String?) = Unit 48 | } 49 | 50 | call(observer) 51 | } 52 | 53 | suspend inline fun setValue( 54 | crossinline call: (SdpObserver) -> Unit 55 | ): Result = suspendCoroutine { 56 | val observer = object : SdpObserver { 57 | /** 58 | * We ignore create results. 59 | */ 60 | override fun onCreateFailure(p0: String?) = Unit 61 | override fun onCreateSuccess(p0: SessionDescription?) = Unit 62 | 63 | /** 64 | * Handling of set values. 65 | */ 66 | override fun onSetSuccess() = it.resume(Result.success(Unit)) 67 | override fun onSetFailure(message: String?) = 68 | it.resume(Result.failure(RuntimeException(message))) 69 | } 70 | 71 | call(observer) 72 | } 73 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/kotlin/io/getstream/webrtc/sample/compose/webrtc/utils/stringify.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.getstream.webrtc.sample.compose.webrtc.utils 18 | 19 | import io.getstream.webrtc.sample.compose.webrtc.peer.StreamPeerType 20 | import org.webrtc.IceCandidateErrorEvent 21 | import org.webrtc.MediaStreamTrack 22 | import org.webrtc.SessionDescription 23 | import org.webrtc.audio.JavaAudioDeviceModule 24 | 25 | fun SessionDescription.stringify(): String = 26 | "SessionDescription(type=$type, description=$description)" 27 | 28 | fun MediaStreamTrack.stringify(): String { 29 | return "MediaStreamTrack(id=${id()}, kind=${kind()}, enabled: ${enabled()}, state=${state()})" 30 | } 31 | 32 | fun IceCandidateErrorEvent.stringify(): String { 33 | return "IceCandidateErrorEvent(errorCode=$errorCode, $errorText, address=$address, port=$port, url=$url)" 34 | } 35 | 36 | fun JavaAudioDeviceModule.AudioSamples.stringify(): String { 37 | return "AudioSamples(audioFormat=$audioFormat, channelCount=$channelCount" + 38 | ", sampleRate=$sampleRate, data.size=${data.size})" 39 | } 40 | 41 | fun StreamPeerType.stringify() = when (this) { 42 | StreamPeerType.PUBLISHER -> "publisher" 43 | StreamPeerType.SUBSCRIBER -> "subscriber" 44 | } 45 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 23 | 24 | 25 | 31 | 34 | 37 | 38 | 39 | 40 | 46 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/drawable/ic_call_end.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/drawable/ic_camera_flip.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 175 | 180 | 185 | 186 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/drawable/ic_mic_off.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/drawable/ic_mic_on.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 25 | 28 | 29 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/drawable/ic_videocam_off.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 23 | 25 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/drawable/ic_videocam_on.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/webrtc-in-jetpack-compose/7a3c1a3d24058ee77e23ff0c86952dd2cd056253/webrtc-android/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/webrtc-in-jetpack-compose/7a3c1a3d24058ee77e23ff0c86952dd2cd056253/webrtc-android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/webrtc-in-jetpack-compose/7a3c1a3d24058ee77e23ff0c86952dd2cd056253/webrtc-android/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/webrtc-in-jetpack-compose/7a3c1a3d24058ee77e23ff0c86952dd2cd056253/webrtc-android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/webrtc-in-jetpack-compose/7a3c1a3d24058ee77e23ff0c86952dd2cd056253/webrtc-android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/webrtc-in-jetpack-compose/7a3c1a3d24058ee77e23ff0c86952dd2cd056253/webrtc-android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/webrtc-in-jetpack-compose/7a3c1a3d24058ee77e23ff0c86952dd2cd056253/webrtc-android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/webrtc-in-jetpack-compose/7a3c1a3d24058ee77e23ff0c86952dd2cd056253/webrtc-android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/webrtc-in-jetpack-compose/7a3c1a3d24058ee77e23ff0c86952dd2cd056253/webrtc-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/webrtc-in-jetpack-compose/7a3c1a3d24058ee77e23ff0c86952dd2cd056253/webrtc-android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | #FFBB86FC 19 | #FF6200EE 20 | #FF3700B3 21 | #FF03DAC5 22 | #FF018786 23 | #FF000000 24 | #FFFFFFFF 25 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | webrtc-sample-compose 19 | 20 | Offline 21 | Second peer is missing 22 | Ready to start session 23 | Creating session 24 | Session Active 25 | 26 | Start session 27 | Join session 28 | 29 | End Session 30 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 22 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 22 | -------------------------------------------------------------------------------- /webrtc-android/app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /webrtc-android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | } 5 | } 6 | 7 | @Suppress("DSL_SCOPE_VIOLATION") 8 | plugins { 9 | alias(libs.plugins.android.application) apply false 10 | alias(libs.plugins.kotlin.jvm) apply false 11 | alias(libs.plugins.kotlin.serialization) apply false 12 | alias(libs.plugins.compose.compiler) apply false 13 | alias(libs.plugins.spotless) apply false 14 | } 15 | 16 | subprojects { 17 | apply(plugin = rootProject.libs.plugins.spotless.get().pluginId) 18 | 19 | tasks.withType().all { 20 | kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() 21 | kotlinOptions.freeCompilerArgs += listOf( 22 | "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", 23 | "-Xopt-in=kotlin.time.ExperimentalTime", 24 | ) 25 | } 26 | 27 | extensions.configure { 28 | kotlin { 29 | target("**/*.kt") 30 | targetExclude("$buildDir/**/*.kt") 31 | ktlint().setUseExperimental(true).editorConfigOverride( 32 | mapOf( 33 | "indent_size" to "2", 34 | "continuation_indent_size" to "2" 35 | ) 36 | ) 37 | licenseHeaderFile(rootProject.file("spotless/copyright.kt")) 38 | trimTrailingWhitespace() 39 | endWithNewline() 40 | } 41 | format("kts") { 42 | target("**/*.kts") 43 | targetExclude("$buildDir/**/*.kts") 44 | licenseHeaderFile(rootProject.file("spotless/copyright.kt"), "(^(?![\\/ ]\\*).*$)") 45 | } 46 | format("xml") { 47 | target("**/*.xml") 48 | targetExclude("**/build/**/*.xml") 49 | licenseHeaderFile(rootProject.file("spotless/copyright.xml"), "(<[^!?])") 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /webrtc-android/buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | repositories { 2 | mavenCentral() 3 | } 4 | 5 | plugins { 6 | `kotlin-dsl` 7 | } 8 | -------------------------------------------------------------------------------- /webrtc-android/buildSrc/src/main/kotlin/Configurations.kt: -------------------------------------------------------------------------------- 1 | object Configurations { 2 | const val compileSdk = 35 3 | const val targetSdk = 35 4 | const val minSdk = 23 5 | const val majorVersion = 1 6 | const val minorVersion = 0 7 | const val patchVersion = 0 8 | const val versionName = "$majorVersion.$minorVersion.$patchVersion" 9 | const val versionCode = 1 10 | } 11 | -------------------------------------------------------------------------------- /webrtc-android/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gradle" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | # Allow up to 10 open pull requests for pip dependencies 13 | open-pull-requests-limit: 10 14 | -------------------------------------------------------------------------------- /webrtc-android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /webrtc-android/gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | streamLog = "1.1.4" 3 | landscapist = "2.4.6" 4 | webrtc = "1.3.6" 5 | androidGradlePlugin = "8.3.2" 6 | androidxActivity = "1.10.0" 7 | androidxAppCompat = "1.7.0" 8 | androidxCompose = "1.7.6" 9 | androidxComposeMaterial3 = "1.3.1" 10 | androidxComposeConstraintLayout = "1.1.0" 11 | androidxComposeNavigation = "2.8.5" 12 | androidxCore = "1.15.0" 13 | androidxLifecycle = "2.8.7" 14 | androidxNavigation = "2.5.0" 15 | kotlin = "2.1.0" 16 | kotlinxCoroutines = "1.9.0" 17 | kotlinxSerializationJson = "1.7.3" 18 | ktlint = "0.43.0" 19 | okhttp = "4.12.0" 20 | retrofit = "2.11.0" 21 | retrofitResultAdapter = "1.0.10" 22 | retrofitKotlinxSerializationJson = "1.0.0" 23 | spotless = "6.7.0" 24 | 25 | [libraries] 26 | webrtc = { group = "io.getstream", name = "stream-webrtc-android", version.ref = "webrtc" } 27 | stream-log = { group = "io.getstream", name = "stream-log-android", version.ref = "streamLog" } 28 | landscapist-glide = { group = "com.github.skydoves", name = "landscapist-glide", version.ref = "landscapist" } 29 | landscapist-animation = { group = "com.github.skydoves", name = "landscapist-animation", version.ref = "landscapist" } 30 | landscapist-placeholder = { group = "com.github.skydoves", name = "landscapist-placeholder", version.ref = "landscapist" } 31 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" } 32 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" } 33 | androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxComposeNavigation" } 34 | androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "androidxCompose" } 35 | androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout", version.ref = "androidxCompose" } 36 | androidx-compose-material-iconsExtended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "androidxCompose" } 37 | androidx-compose-material = { group = "androidx.compose.material", name = "material", version.ref = "androidxCompose" } 38 | androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidxComposeMaterial3" } 39 | androidx-compose-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "androidxComposeConstraintLayout" } 40 | androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "androidxCompose" } 41 | androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "androidxCompose" } 42 | androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "androidxCompose" } 43 | androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "androidxCompose" } 44 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidxCore" } 45 | androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" } 46 | androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" } 47 | kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } 48 | kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } 49 | okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } 50 | retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } 51 | retrofit-result-adapter = { group = "com.github.skydoves", name = "retrofit-adapters-result", version.ref = "retrofitResultAdapter" } 52 | retrofit-kotlin-serialization = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofitKotlinxSerializationJson" } 53 | 54 | # Dependencies of the included build-logic 55 | android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } 56 | kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } 57 | spotless-gradlePlugin = { group = "com.diffplug.spotless", name = "spotless-plugin-gradle", version.ref = "spotless" } 58 | 59 | [plugins] 60 | android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } 61 | android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } 62 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 63 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 64 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 65 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 66 | spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } 67 | -------------------------------------------------------------------------------- /webrtc-android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/webrtc-in-jetpack-compose/7a3c1a3d24058ee77e23ff0c86952dd2cd056253/webrtc-android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /webrtc-android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Oct 19 09:43:34 KST 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /webrtc-android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /webrtc-android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /webrtc-android/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | maven(url = "https://jitpack.io") 14 | maven(url = "https://plugins.gradle.org/m2/") 15 | } 16 | } 17 | rootProject.name = "webrtc-sample-compose" 18 | include(":app") 19 | -------------------------------------------------------------------------------- /webrtc-android/spotless/copyright.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | -------------------------------------------------------------------------------- /webrtc-android/spotless/copyright.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Stream.IO, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2. 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | -------------------------------------------------------------------------------- /webrtc-android/spotless/copyright.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | -------------------------------------------------------------------------------- /webrtc-android/spotless/spotless.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.diffplug.spotless" 2 | 3 | spotless { 4 | kotlin { 5 | target "**/*.kt" 6 | targetExclude "**/build/**/*.kt" 7 | ktlint().setUseExperimental(true).editorConfigOverride(['indent_size': '2', 'continuation_indent_size': '2']) 8 | licenseHeaderFile "$rootDir/spotless/copyright.kt" 9 | trimTrailingWhitespace() 10 | endWithNewline() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /webrtc-backend/.gitignore: -------------------------------------------------------------------------------- 1 | /.gradle 2 | /.idea 3 | /out 4 | /build 5 | *.iml 6 | *.ipr 7 | *.iws 8 | -------------------------------------------------------------------------------- /webrtc-backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official Gradle 6.6.1 image with JDK 17 as the build environment 2 | FROM gradle:6.6.1-jdk11 AS build 3 | 4 | # Set the working directory inside the container 5 | WORKDIR /app 6 | 7 | # Copy the project files into the container 8 | COPY . . 9 | 10 | # Set JVM compatibility to 17 in the build process 11 | RUN gradle clean build -Dorg.gradle.java.home=/opt/java/openjdk -x test --no-daemon 12 | 13 | # Use the official OpenJDK 17 runtime as the runtime environment 14 | FROM openjdk:17-jdk-slim 15 | 16 | # Set the working directory inside the container 17 | WORKDIR /app 18 | 19 | # Copy the built JAR from the build environment 20 | COPY --from=build /app/build/libs/*.jar app.jar 21 | 22 | # Expose the port that the application will run on 23 | EXPOSE 8080 24 | 25 | # Set environment variables if needed (e.g., for ports) 26 | ENV PORT=8080 27 | 28 | # Run the application 29 | CMD ["java", "-jar", "app.jar"] 30 | -------------------------------------------------------------------------------- /webrtc-backend/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | 6 | dependencies { 7 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 8 | } 9 | } 10 | 11 | apply plugin: 'kotlin' 12 | apply plugin: 'application' 13 | 14 | group 'com.webrtc.ktor.server' 15 | version '0.0.1' 16 | mainClassName = "io.ktor.server.netty.EngineMain" 17 | 18 | sourceSets { 19 | main.kotlin.srcDirs = main.java.srcDirs = ['src'] 20 | test.kotlin.srcDirs = test.java.srcDirs = ['test'] 21 | main.resources.srcDirs = ['resources'] 22 | test.resources.srcDirs = ['testresources'] 23 | } 24 | 25 | repositories { 26 | mavenLocal() 27 | jcenter() 28 | maven { url 'https://kotlin.bintray.com/ktor' } 29 | } 30 | 31 | dependencies { 32 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" 33 | implementation "io.ktor:ktor-server-netty:$ktor_version" 34 | implementation "ch.qos.logback:logback-classic:$logback_version" 35 | implementation "io.ktor:ktor-client-core:$ktor_version" 36 | implementation "io.ktor:ktor-client-core-jvm:$ktor_version" 37 | implementation "io.ktor:ktor-client-cio:$ktor_version" 38 | implementation "io.ktor:ktor-server-core:$ktor_version" 39 | implementation "io.ktor:ktor-websockets:$ktor_version" 40 | implementation "io.ktor:ktor-client-websockets:$ktor_version" 41 | implementation "io.ktor:ktor-client-logging-jvm:$ktor_version" 42 | testImplementation "io.ktor:ktor-server-tests:$ktor_version" 43 | } 44 | 45 | // JAR task configuration to include the main class in the manifest 46 | jar { 47 | manifest { 48 | attributes( 49 | 'Main-Class': mainClassName, // Sets the Main-Class attribute for JAR execution 50 | 'Implementation-Title': project.name, 51 | 'Implementation-Version': project.version 52 | ) 53 | } 54 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE // Exclude duplicate files 55 | from { 56 | configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } // Include dependencies in the JAR 57 | } 58 | } -------------------------------------------------------------------------------- /webrtc-backend/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | webrtc_ktro_server: 5 | # image: DOCKER IMAGE NAME 6 | build: . 7 | ports: 8 | - "8080:8080" 9 | restart: always 10 | -------------------------------------------------------------------------------- /webrtc-backend/gradle.properties: -------------------------------------------------------------------------------- 1 | ktor_version=1.6.4 2 | kotlin.code.style=official 3 | kotlin_version=1.5.31 4 | logback_version=1.2.1 5 | -------------------------------------------------------------------------------- /webrtc-backend/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GetStream/webrtc-in-jetpack-compose/7a3c1a3d24058ee77e23ff0c86952dd2cd056253/webrtc-backend/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /webrtc-backend/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /webrtc-backend/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /webrtc-backend/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /webrtc-backend/resources/application.conf: -------------------------------------------------------------------------------- 1 | ktor { 2 | deployment { 3 | port = 8080 4 | port = ${?PORT} 5 | } 6 | application { 7 | modules = [ ApplicationKt.module ] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /webrtc-backend/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /webrtc-backend/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "webrtc_ktor_server" 2 | -------------------------------------------------------------------------------- /webrtc-backend/src/Application.kt: -------------------------------------------------------------------------------- 1 | import io.ktor.application.* 2 | import io.ktor.http.cio.websocket.* 3 | import io.ktor.response.* 4 | import io.ktor.routing.* 5 | import io.ktor.websocket.* 6 | import kotlinx.coroutines.channels.ClosedReceiveChannelException 7 | import java.time.Duration 8 | import java.util.* 9 | 10 | /** 11 | * Originally written by Artem Bagritsevich. 12 | * 13 | * https://github.com/artem-bagritsevich/WebRTCKtorSignalingServerExample 14 | */ 15 | fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args) 16 | 17 | @Suppress("unused") // Referenced in application.conf 18 | @JvmOverloads 19 | fun Application.module(testing: Boolean = false) { 20 | 21 | install(WebSockets) { 22 | pingPeriod = Duration.ofSeconds(15) 23 | timeout = Duration.ofSeconds(15) 24 | maxFrameSize = Long.MAX_VALUE 25 | masking = false 26 | } 27 | 28 | routing { 29 | 30 | get("/") { 31 | call.respond("Hello from WebRTC signaling server") 32 | } 33 | webSocket("/rtc") { 34 | val sessionID = UUID.randomUUID() 35 | try { 36 | SessionManager.onSessionStarted(sessionID, this) 37 | 38 | for (frame in incoming) { 39 | when (frame) { 40 | is Frame.Text -> { 41 | SessionManager.onMessage(sessionID, frame.readText()) 42 | } 43 | 44 | else -> Unit 45 | } 46 | } 47 | println("Exiting incoming loop, closing session: $sessionID") 48 | SessionManager.onSessionClose(sessionID) 49 | } catch (e: ClosedReceiveChannelException) { 50 | println("onClose $sessionID") 51 | SessionManager.onSessionClose(sessionID) 52 | } catch (e: Throwable) { 53 | println("onError $sessionID $e") 54 | SessionManager.onSessionClose(sessionID) 55 | } 56 | } 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /webrtc-backend/src/SessionManager.kt: -------------------------------------------------------------------------------- 1 | import io.ktor.http.cio.websocket.* 2 | import io.ktor.websocket.* 3 | import kotlinx.coroutines.* 4 | import kotlinx.coroutines.sync.Mutex 5 | import kotlinx.coroutines.sync.withLock 6 | import java.util.* 7 | 8 | /** 9 | * Originally written by Artem Bagritsevich. 10 | * 11 | * https://github.com/artem-bagritsevich/WebRTCKtorSignalingServerExample 12 | */ 13 | object SessionManager { 14 | 15 | private val sessionManagerScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) 16 | private val mutex = Mutex() 17 | 18 | private val clients = mutableMapOf() 19 | 20 | private var sessionState: WebRTCSessionState = WebRTCSessionState.Impossible 21 | 22 | fun onSessionStarted(sessionId: UUID, session: DefaultWebSocketServerSession) { 23 | sessionManagerScope.launch { 24 | mutex.withLock { 25 | if (clients.size > 1) { 26 | sessionManagerScope.launch(NonCancellable) { 27 | session.send(Frame.Close()) // only two peers are supported 28 | } 29 | return@launch 30 | } 31 | clients[sessionId] = session 32 | session.send("Added as a client: $sessionId") 33 | if (clients.size > 1) { 34 | sessionState = WebRTCSessionState.Ready 35 | } 36 | notifyAboutStateUpdate() 37 | } 38 | } 39 | } 40 | 41 | fun onMessage(sessionId: UUID, message: String) { 42 | when { 43 | message.startsWith(MessageType.STATE.toString(), true) -> handleState(sessionId) 44 | message.startsWith(MessageType.OFFER.toString(), true) -> handleOffer(sessionId, message) 45 | message.startsWith(MessageType.ANSWER.toString(), true) -> handleAnswer(sessionId, message) 46 | message.startsWith(MessageType.ICE.toString(), true) -> handleIce(sessionId, message) 47 | } 48 | } 49 | 50 | private fun handleState(sessionId: UUID) { 51 | sessionManagerScope.launch { 52 | clients[sessionId]?.send("${MessageType.STATE} $sessionState") 53 | } 54 | } 55 | 56 | private fun handleOffer(sessionId: UUID, message: String) { 57 | if (sessionState != WebRTCSessionState.Ready) { 58 | error("Session should be in Ready state to handle offer") 59 | } 60 | sessionState = WebRTCSessionState.Creating 61 | println("handling offer from $sessionId") 62 | notifyAboutStateUpdate() 63 | val clientToSendOffer = clients.filterKeys { it != sessionId }.values.first() 64 | clientToSendOffer.send(message) 65 | } 66 | 67 | private fun handleAnswer(sessionId: UUID, message: String) { 68 | if (sessionState != WebRTCSessionState.Creating) { 69 | error("Session should be in Creating state to handle answer") 70 | } 71 | println("handling answer from $sessionId") 72 | val clientToSendAnswer = clients.filterKeys { it != sessionId }.values.first() 73 | clientToSendAnswer.send(message) 74 | sessionState = WebRTCSessionState.Active 75 | notifyAboutStateUpdate() 76 | } 77 | 78 | private fun handleIce(sessionId: UUID, message: String) { 79 | println("handling ice from $sessionId") 80 | val clientToSendIce = clients.filterKeys { it != sessionId }.values.first() 81 | clientToSendIce.send(message) 82 | } 83 | 84 | fun onSessionClose(sessionId: UUID) { 85 | sessionManagerScope.launch { 86 | mutex.withLock { 87 | clients.remove(sessionId) 88 | sessionState = WebRTCSessionState.Impossible 89 | notifyAboutStateUpdate() 90 | } 91 | } 92 | } 93 | 94 | enum class WebRTCSessionState { 95 | Active, // Offer and Answer messages has been sent 96 | Creating, // Creating session, offer has been sent 97 | Ready, // Both clients available and ready to initiate session 98 | Impossible // We have less than two clients 99 | } 100 | 101 | enum class MessageType { 102 | STATE, 103 | OFFER, 104 | ANSWER, 105 | ICE 106 | } 107 | 108 | private fun notifyAboutStateUpdate() { 109 | clients.forEach { (_, client) -> 110 | client.send("${MessageType.STATE} $sessionState") 111 | } 112 | } 113 | 114 | private fun DefaultWebSocketServerSession.send(message: String) { 115 | sessionManagerScope.launch { 116 | this@send.send(Frame.Text(message)) 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /webrtc-backend/src/TestWsClient.kt: -------------------------------------------------------------------------------- 1 | import io.ktor.client.* 2 | import io.ktor.client.engine.cio.* 3 | import io.ktor.client.features.websocket.* 4 | import io.ktor.http.* 5 | import io.ktor.http.cio.websocket.* 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.channels.consumeEach 8 | import kotlinx.coroutines.delay 9 | import kotlinx.coroutines.launch 10 | import kotlinx.coroutines.runBlocking 11 | 12 | /** 13 | * Originally written by Artem Bagritsevich. 14 | * 15 | * https://github.com/artem-bagritsevich/WebRTCKtorSignalingServerExample 16 | */ 17 | object TestWsClient1 { 18 | @JvmStatic 19 | fun main(args: Array) { 20 | runBlocking { 21 | val client = HttpClient(CIO).config { install(WebSockets) } 22 | client.ws(method = HttpMethod.Get, host = "127.0.0.1", port = 8080, path = "/rtc") { 23 | launch(Dispatchers.Default) { 24 | delay(15000L) 25 | send("${SessionManager.MessageType.OFFER} SDP asdaskfslkdfnlskdnfglksdnfklnsdkf") 26 | } 27 | incoming.consumeEach { frame -> 28 | when (frame) { 29 | is Frame.Text -> { 30 | println(frame.readText()) 31 | } 32 | 33 | else -> Unit 34 | } 35 | } 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * Used only for testing, subscribes for the offer and sends answer if offer received 42 | */ 43 | object TestWsClient2 { 44 | @JvmStatic 45 | fun main(args: Array) { 46 | runBlocking { 47 | val client = HttpClient(CIO).config { install(WebSockets) } 48 | client.ws(method = HttpMethod.Get, host = "127.0.0.1", port = 8080, path = "/rtc") { 49 | incoming.consumeEach { frame -> 50 | when (frame) { 51 | is Frame.Text -> { 52 | val text = frame.readText() 53 | println(text) 54 | if (text.startsWith("offer", true)) { 55 | send("${SessionManager.MessageType.ANSWER} SDP saknfaslkdjflskdjfklnsdfasdasdsd") 56 | } 57 | } 58 | 59 | else -> Unit 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /webrtc-backend/test/ApplicationTest.kt: -------------------------------------------------------------------------------- 1 | package com.webrtc.ktor.server 2 | 3 | import io.ktor.application.* 4 | import io.ktor.response.* 5 | import io.ktor.request.* 6 | import io.ktor.client.* 7 | import io.ktor.client.engine.cio.* 8 | import io.ktor.routing.* 9 | import io.ktor.http.* 10 | import io.ktor.websocket.* 11 | import io.ktor.http.cio.websocket.* 12 | import java.time.* 13 | import io.ktor.client.features.websocket.* 14 | import io.ktor.client.features.websocket.WebSockets 15 | import io.ktor.http.cio.websocket.Frame 16 | import kotlinx.coroutines.* 17 | import kotlinx.coroutines.channels.* 18 | import io.ktor.client.features.logging.* 19 | import kotlin.test.* 20 | import io.ktor.server.testing.* 21 | import module 22 | 23 | class ApplicationTest { 24 | @Test 25 | fun testRoot() { 26 | withTestApplication({ module(testing = true) }) { 27 | handleRequest(HttpMethod.Get, "/").apply { 28 | assertEquals(HttpStatusCode.OK, response.status()) 29 | assertEquals("HELLO WORLD!", response.content) 30 | } 31 | } 32 | } 33 | } 34 | --------------------------------------------------------------------------------