├── .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 | 
2 |
3 |
WebRTC in Jetpack Compose
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
54 |
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 |
--------------------------------------------------------------------------------