├── .github
└── workflows
│ ├── build.yml
│ ├── create-documentation-pr.yml
│ ├── create-release-from-changelog.yml
│ ├── release.yml
│ └── update-documentation.yml
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── build.gradle
├── docs
├── docs_logo.svg
└── logo-icon.svg
├── example
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── video
│ │ └── api
│ │ └── livestream
│ │ └── example
│ │ └── ui
│ │ ├── main
│ │ ├── MainActivity.kt
│ │ ├── PreviewFragment.kt
│ │ └── PreviewViewModel.kt
│ │ ├── preferences
│ │ ├── PreferencesActivity.kt
│ │ └── PreferencesFragment.kt
│ │ └── utils
│ │ ├── Configuration.kt
│ │ └── DialogHelper.kt
│ └── res
│ ├── drawable
│ ├── circle_shape.xml
│ ├── circle_toggle_button.xml
│ ├── circle_toggle_button_off.xml
│ ├── circle_toggle_button_on.xml
│ ├── ic_api_video.xml
│ ├── ic_baseline_mic_off_24.xml
│ └── ic_baseline_switch_camera_24.xml
│ ├── layout
│ ├── activity_main.xml
│ ├── activity_preferences.xml
│ └── fragment_preview.xml
│ ├── menu
│ └── menu_main.xml
│ ├── values
│ ├── arrays.xml
│ ├── colors.xml
│ ├── strings.xml
│ └── themes.xml
│ └── xml
│ └── preferences.xml
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── livestream
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── maven-push.gradle
├── proguard-rules.pro
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── video
│ │ │ └── api
│ │ │ └── livestream
│ │ │ ├── ApiVideoLiveStream.kt
│ │ │ ├── ConfigurationHelper.kt
│ │ │ ├── Extensions.kt
│ │ │ ├── enums
│ │ │ ├── CameraFacingDirection.kt
│ │ │ └── Resolution.kt
│ │ │ ├── interfaces
│ │ │ └── IConnectionListener.kt
│ │ │ ├── models
│ │ │ ├── AudioConfig.kt
│ │ │ └── VideoConfig.kt
│ │ │ └── views
│ │ │ └── ApiVideoView.kt
│ └── res
│ │ └── values
│ │ └── strings.xml
│ └── test
│ └── java
│ └── video
│ └── api
│ └── livestream
│ └── ExtensionsKtTest.kt
└── settings.gradle
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v4
10 | - name: Set up Java
11 | uses: actions/setup-java@v3
12 | with:
13 | java-version: '18'
14 | distribution: 'adopt'
15 | - name: Grant execute permission for gradlew
16 | run: chmod +x gradlew
17 | - name: Build and run tests
18 | run: ./gradlew build
19 |
--------------------------------------------------------------------------------
/.github/workflows/create-documentation-pr.yml:
--------------------------------------------------------------------------------
1 | name: Create documentation PR
2 | on:
3 | # Trigger the workflow on pull requests targeting the main branch
4 | pull_request:
5 | types: [assigned, unassigned, opened, reopened, synchronize, edited, labeled, unlabeled, edited, closed]
6 | branches:
7 | - main
8 |
9 | jobs:
10 | create_documentation_pr:
11 | if: github.event.action != 'closed'
12 |
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Check out current repository code
17 | uses: actions/checkout@v2
18 |
19 | - name: Create the documentation pull request
20 | uses: apivideo/api.video-create-readme-file-pull-request-action@main
21 | with:
22 | source-file-path: "README.md"
23 | destination-repository: apivideo/api.video-documentation
24 | destination-path: sdks/livestream
25 | destination-filename: apivideo-android-livestream-module.md
26 | pat: "${{ secrets.PAT }}"
27 |
--------------------------------------------------------------------------------
/.github/workflows/create-release-from-changelog.yml:
--------------------------------------------------------------------------------
1 | name: Create draft release from CHANGELOG.md
2 |
3 | on:
4 | push:
5 | paths:
6 | - 'CHANGELOG.md'
7 |
8 | jobs:
9 | update-documentation:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 | - name: Create draft release if needed
14 | uses: apivideo/api.video-release-from-changelog-action@main
15 | with:
16 | github-auth-token: ${{ secrets.GITHUB_TOKEN }}
17 | prefix: v
18 |
19 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Publish package to the Maven Central Repository
2 |
3 | on:
4 | release:
5 | types: [ published ]
6 |
7 | jobs:
8 | publish:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - name: Set up Java
13 | uses: actions/setup-java@v3
14 | with:
15 | java-version: '18'
16 | distribution: 'adopt'
17 | - name: Grant execute permission for gradlew
18 | run: chmod +x gradlew
19 | - name: Decode the secret key
20 | run: echo $GPG_KEYRING_FILE_CONTENT | base64 --decode > ~/secring.gpg
21 | env:
22 | GPG_KEYRING_FILE_CONTENT: "${{ secrets.GPG_KEYRING_FILE_CONTENT }}"
23 | - name: Publish package
24 | run: ./gradlew publish -Psigning.secretKeyRingFile=$(echo ~/secring.gpg) -Psigning.password=$GPG_PASSWORD -Psigning.keyId=$GPG_KEY_ID
25 | env:
26 | NEXUS_USERNAME: ${{ secrets.OSSRH_USERNAME }}
27 | NEXUS_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
28 | GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }}
29 | GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }}
--------------------------------------------------------------------------------
/.github/workflows/update-documentation.yml:
--------------------------------------------------------------------------------
1 | name: Update readme.io documentation
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | update-api-documentation:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - name: Set up Java
13 | uses: actions/setup-java@v3
14 | with:
15 | java-version: '18'
16 | distribution: 'adopt'
17 | - name: Grant execute permission for gradlew
18 | run: chmod +x gradlew
19 | - name: Generate API documentation
20 | run: ./gradlew dokkaHtml
21 | - name: Deploy API documentation to Github Pages
22 | uses: JamesIves/github-pages-deploy-action@v4
23 | with:
24 | token: ${{ secrets.GITHUB_TOKEN }}
25 | branch: gh-pages
26 | folder: livestream/build/dokka/html
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Gradle/build
2 | .gradle
3 | /build
4 | .externalNativeBuild
5 | .cxx
6 |
7 | # Intellij
8 | *.iml
9 | .idea/
10 |
11 | # Local file
12 | local.properties
13 |
14 | # Mac
15 | .DS_Store
16 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All changes to this project will be documented in this file.
4 |
5 | ## [1.4.3] - 2024-10-31
6 |
7 | - Try/catch on `startPreview` calls
8 | - Upgrade dependencies
9 |
10 | ## [1.4.2] - 2024-07-11
11 |
12 | - Update to StreamPack 2.6.1
13 | - View uses PreviewView
14 |
15 | ## [1.4.1] - 2024-03-01
16 |
17 | - Fix preview aspect ratio
18 | - Fix 480p resolution
19 | - Fix crashes due to missing permissions
20 |
21 | ## [1.4.0] - 2024-02-15
22 |
23 | - Add an API to explicitly set the camera to use
24 | - Add an API to set the video resolution with a `Size` object
25 | - Rename `IConnectionChecker` to `IConnectionListener`
26 | - Add a callback in `ApiVideoLiveStream` constructor to know when the library requires the
27 | permission to access the camera or the microphone
28 | - Update to StreamPack 2.6.0
29 | - Upgrade to gradle 8, Kotlin 1.9
30 |
31 | ## [1.3.1] - 2023-03-27
32 |
33 | - Return a `onConnectionFailed` when `connectStream` failed.
34 |
35 | ## [1.3.0] - 2023-01-06
36 |
37 | - Add an API to set the interval between to key frames
38 | - Synchronize video and audio RTMP packets
39 | - Fix a crash when microphone is muted on few devices
40 |
41 | ## [1.2.3] - 2022-10-10
42 |
43 | - Fix a crash on `stopStreaming` due to a `free` in `rtmpdroid`
44 |
45 | ## [1.2.2] - 2022-10-05
46 |
47 | - Fix preview when `videoConfig` is set before the `view.display` exists
48 |
49 | ## [1.2.1] - 2022-09-29
50 |
51 | - Fix preview when `ApiVideoView` has already been created
52 | - Only call `onDisconnect` when application was connected
53 | - Release workflow is triggered on release published (instead of created)
54 | - Example: remove rxpermission usage
55 |
56 | ## [1.2.0] - 2022-08-18
57 |
58 | - Adds API to set zoom ratio
59 |
60 | ## [1.1.0] - 2022-08-05
61 |
62 | - `initialVideoConfig` and `initialAudioConfig` are now optional
63 | - Multiple fixes on RTMP stream (to avoid ANR and to improve compatibility)
64 |
65 | ## [1.0.4] - 2022-06-28
66 |
67 | - Disconnect after a `stopStream`.
68 |
69 | ## [1.0.3] - 2022-06-13
70 |
71 | - Fix stream after a `stopPreview` call.
72 | - Disconnect if `startStream` fails.
73 |
74 | ## [1.0.2] - 2022-04-25
75 |
76 | - Do not remove SurfaceView callbacks when the Surface is destroyed.
77 |
78 | ## [1.0.1] - 2022-04-13
79 |
80 | - Fix audioConfig and videoConfig API
81 | - Improve stop live button look
82 |
83 | ## [1.0.0] - 2022-04-05
84 |
85 | - Add a configuration helper
86 | - Add video and audio configuration default value instead of using a builder
87 | - Change internal RTMP live stream library
88 |
89 | ## [0.3.3] - 2022-01-24
90 |
91 | - Add startPreview/stopPreview API
92 |
93 | ## [0.3.2] - 2022-01-19
94 |
95 | - Catch onConnectionFailed to stop streaming without user
96 | - Throw an exception on `startStreaming` when stream key is empty
97 | - Remove jcenter as a dependency repository
98 |
99 | ## [0.3.1] - 2021-12-14
100 |
101 | - Add a trailing slash at the end of the RTMP url in case it is missing
102 | - Rename project to live-stream
103 |
104 | ## [0.3.0] - 2021-10-14
105 |
106 | - Add/Improve API: introducing videoConfig and audioConfig changes
107 |
108 | ## [0.3.0] - 2021-10-14
109 |
110 | - Add/Improve API: introducing videoConfig and audioConfig changes
111 |
112 | ## [0.2.0] - 2021-10-07
113 |
114 | - Sample application
115 |
116 | ## [0.1.0] - 2021-05-14
117 |
118 | - First version
119 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to api.video
2 |
3 | :movie_camera::+1::tada: Thank you for taking the time to contribute and participate in the implementation of a Video First World! :tada::+1::movie_camera:
4 |
5 | The following is a set of guidelines for contributing to api.video and its packages, which are hosted in the [api.video Organization](https://github.com/apivideo) on GitHub.
6 |
7 | #### Table of contents
8 |
9 | - [Contributing to api.video](#contributing-to-apivideo)
10 | - [Table of contents](#table-of-contents)
11 | - [Code of conduct](#code-of-conduct)
12 | - [I just have a question!](#i-just-have-a-question)
13 | - [How can I contribute?](#how-can-i-contribute)
14 | - [Reporting bugs](#reporting-bugs)
15 | - [Before submitting a bug report](#before-submitting-a-bug-report)
16 | - [How do I submit a (good) bug report?](#how-do-i-submit-a-good-bug-report)
17 | - [Suggesting enhancements](#suggesting-enhancements)
18 | - [How do I submit a (good) enhancement suggestion?](#how-do-i-submit-a-good-enhancement-suggestion)
19 | - [Pull requests](#pull-requests)
20 | - [Style guides](#style-guides)
21 | - [Git commit messages](#git-commit-messages)
22 | - [Documentation style guide](#documentation-style-guide)
23 | - [Additional notes](#additional-notes)
24 | - [Issue and pull request labels](#issue-and-pull-request-labels)
25 | - [Type of issue and issue state](#type-of-issue-and-issue-state)
26 | - [Topic categories](#topic-categories)
27 | - [Pull request labels](#pull-request-labels)
28 |
29 | ## Code of conduct
30 |
31 | This project and everyone participating in it is governed by the [api.video Code of Conduct](https://github.com/apivideo/.github/blob/main/CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [help@api.video](mailto:help@api.video).
32 |
33 | ## I just have a question!
34 |
35 | > **Note:** [Please don't file an issue to ask a question.] You'll get faster results by using the resources below.
36 |
37 | We have an official message board with a detailed FAQ and where the community chimes in with helpful advice if you have questions.
38 |
39 | * [The official api.video's Community](https://community.api.video/)
40 | * [api.video FAQ](https://community.api.video/c/faq/)
41 |
42 |
43 | ## How can I contribute?
44 |
45 | ### Reporting bugs
46 |
47 | This section guides you through submitting a bug report for api.video. Following these guidelines helps maintainers and the community understand your report :pencil:, reproduce the behavior :computer:, and find related reports :mag_right:.
48 |
49 | Before creating bug reports, please check [this list](#before-submitting-a-bug-report) as you might find out that you don't need to create one. When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). Fill out [the required template](https://github.com/apivideo/.github/blob/main/.github/ISSUE_TEMPLATE/bug_report.yml), the information it asks for helps us resolve issues faster.
50 |
51 | > **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one.
52 |
53 | #### Before submitting a bug report
54 |
55 | * **Check the [The official api.video's Community](https://community.api.video/)** for a list of common questions and problems.
56 | * **Determine which repository the problem should be reported in**.
57 | * **Perform a [cursory search](https://github.com/search?q=is%3Aissue+user%3Aapivideo)** to see if the problem has already been reported. If it has **and the issue is still open**, add a comment to the existing issue instead of opening a new one.
58 |
59 | #### How do I submit a (good) bug report?
60 |
61 | Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined which repository your bug is related to, create an issue on that repository and provide the following information by filling in [the template](https://github.com/apivideo/.github/blob/main/.github/ISSUE_TEMPLATE/bug_report.yml).
62 |
63 | Explain the problem and include additional details to help maintainers reproduce the problem:
64 |
65 | * **Use a clear and descriptive title** for the issue to identify the problem.
66 | * **Describe the exact steps which reproduce the problem** in as many details as possible. When listing steps, **don't just say what you did, but explain how you did it**.
67 | * **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
68 | * **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior.
69 | * **Explain which behavior you expected to see instead and why.**
70 | * **Include screenshots or videos** which show you following the described steps and clearly demonstrate the problem.
71 | * **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below.
72 |
73 | Provide more context by answering these questions:
74 |
75 | * **Did the problem start happening recently** (e.g. after updating to a new version of api.video) or was this always a problem?
76 | * If the problem started happening recently.**
77 | * **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens.
78 |
79 | Include details about your configuration and environment:
80 |
81 | * **Which version of the api.video package are you using?**
82 | * **What's the name and version of the OS you're using?**
83 |
84 | ### Suggesting enhancements
85 |
86 | This section guides you through submitting an enhancement suggestion for api.video project, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion :pencil: and find related suggestions :mag_right:.
87 |
88 | When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion). Fill in [the template](https://github.com/apivideo/.github/blob/main/.github/ISSUE_TEMPLATE/feature_request.yml), including the steps that you imagine you would take if the feature you're requesting existed.
89 |
90 |
91 | #### How do I submit a (good) enhancement suggestion?
92 |
93 | Enhancement suggestions are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you've determined which repository your enhancement suggestion is related to, create an issue on that repository and provide the following information:
94 |
95 | * **Use a clear and descriptive title** for the issue to identify the suggestion.
96 | * **Provide a step-by-step description of the suggested enhancement** in as many details as possible.
97 | * **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines).
98 | * **Describe the current behavior** and **explain which behavior you expected to see instead** and why.
99 | * **Include screenshots** which help you demonstrate the steps or point out the part of api.video which the suggestion is related to.
100 | * **Explain why this enhancement would be useful** to most api.video users and isn't something that can or should be implemented as a community package.
101 | * **Specify which version of the api.video package you're using.**
102 | * **Specify the name and version of the OS you're using.**
103 |
104 |
105 | ### Pull requests
106 |
107 | The process described here has several goals:
108 |
109 | - Maintain api.video's quality
110 | - Fix problems that are important to users
111 | - Engage the community in working toward the best possible api.video
112 | - Enable a sustainable system for api.video's maintainers to review contributions
113 |
114 | Please follow these steps to have your contribution considered by the maintainers:
115 |
116 | 1. Explain what, why and how you resolved the issue. If you have a related issue, please mention it.
117 | 2. Follow the [style guides](#style-guides)
118 | 3. After you submit your pull request, verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing What if the status checks are failing?If a status check is failing, and you believe that the failure is unrelated to your change, please leave a comment on the pull request explaining why you believe the failure is unrelated. A maintainer will re-run the status check for you. If we conclude that the failure was a false positive, then we will open an issue to track that problem with our status check suite.
119 |
120 | While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted.
121 |
122 | ## Style guides
123 |
124 | ### Git commit messages
125 |
126 | * Use the present tense ("Add feature" not "Added feature")
127 | * Limit the first line to 72 characters or less
128 | * Reference issues and pull requests after the first line
129 | * Consider starting the commit message with an applicable emoji:
130 | * :art: `:art:` when improving the format/structure of the code
131 | * :racehorse: `:racehorse:` when improving performance
132 | * :non-potable_water: `:non-potable_water:` when plugging memory leaks
133 | * :memo: `:memo:` when writing docs
134 | * :penguin: `:penguin:` when fixing something on Linux
135 | * :apple: `:apple:` when fixing something on macOS
136 | * :checkered_flag: `:checkered_flag:` when fixing something on Windows
137 | * :bug: `:bug:` when fixing a bug
138 | * :fire: `:fire:` when removing code or files
139 | * :green_heart: `:green_heart:` when fixing the CI build
140 | * :white_check_mark: `:white_check_mark:` when adding tests
141 | * :lock: `:lock:` when dealing with security
142 | * :arrow_up: `:arrow_up:` when upgrading dependencies
143 | * :arrow_down: `:arrow_down:` when downgrading dependencies
144 | * :shirt: `:shirt:` when removing linter warnings
145 |
146 | ### Documentation style guide
147 |
148 | * Use [Markdown](https://daringfireball.net/projects/markdown).
149 |
150 |
151 | ## Additional notes
152 |
153 | ### Issue and pull request labels
154 |
155 | This section lists the labels we use to help us track and manage issues and pull requests on all api.video repositories.
156 |
157 | [GitHub search](https://help.github.com/articles/searching-issues/) makes it easy to use labels for finding groups of issues or pull requests you're interested in. We encourage you to read about [other search filters](https://help.github.com/articles/searching-issues/) which will help you write more focused queries.
158 |
159 |
160 | #### Type of issue and issue state
161 |
162 | | Label name | `apivideo` :mag_right: | Description |
163 | | --- | --- | --- |
164 | | `enhancement` | [search][search-apivideo-org-label-enhancement] | Feature requests. |
165 | | `bug` | [search][search-apivideo-org-label-bug] | Confirmed bugs or reports that are very likely to be bugs. |
166 | | `question` | [search][search-apivideo-org-label-question] | Questions more than bug reports or feature requests (e.g. how do I do X). |
167 | | `feedback` | [search][search-apivideo-org-label-feedback] | General feedback more than bug reports or feature requests. |
168 | | `help-wanted` | [search][search-apivideo-org-label-help-wanted] | The api.video team would appreciate help from the community in resolving these issues. |
169 | | `more-information-needed` | [search][search-apivideo-org-label-more-information-needed] | More information needs to be collected about these problems or feature requests (e.g. steps to reproduce). |
170 | | `needs-reproduction` | [search][search-apivideo-org-label-needs-reproduction] | Likely bugs, but haven't been reliably reproduced. |
171 | | `blocked` | [search][search-apivideo-org-label-blocked] | Issues blocked on other issues. |
172 | | `duplicate` | [search][search-apivideo-org-label-duplicate] | Issues which are duplicates of other issues, i.e. they have been reported before. |
173 | | `wontfix` | [search][search-apivideo-org-label-wontfix] | The api.video team has decided not to fix these issues for now, either because they're working as intended or for some other reason. |
174 | | `invalid` | [search][search-apivideo-org-label-invalid] | Issues which aren't valid (e.g. user errors). |
175 | | `package-idea` | [search][search-apivideo-org-label-package-idea] | Feature request which might be good candidates for new packages, instead of extending api.video packages. |
176 | | `wrong-repo` | [search][search-apivideo-org-label-wrong-repo] | Issues reported on the wrong repository. |
177 |
178 | #### Topic categories
179 |
180 | | Label name | `apivideo` :mag_right: | Description |
181 | | --- | --- | --- |
182 | | `windows` | [search][search-apivideo-org-label-windows] | Related to api.video running on Windows. |
183 | | `linux` | [search][search-apivideo-org-label-linux] | Related to api.video running on Linux. |
184 | | `mac` | [search][search-apivideo-org-label-mac] | Related to api.video running on macOS. |
185 | | `documentation` | [search][search-apivideo-org-label-documentation] | Related to any type of documentation. |
186 | | `performance` | [search][search-apivideo-org-label-performance] | Related to performance. |
187 | | `security` | [search][search-apivideo-org-label-security] | Related to security. |
188 | | `ui` | [search][search-apivideo-org-label-ui] | Related to visual design. |
189 | | `api` | [search][search-apivideo-org-label-api] | Related to api.video's public APIs. |
190 |
191 | #### Pull request labels
192 |
193 | | Label name | `apivideo` :mag_right: | Description
194 | | --- | --- | --- |
195 | | `work-in-progress` | [search][search-apivideo-org-label-work-in-progress] | Pull requests which are still being worked on, more changes will follow. |
196 | | `needs-review` | [search][search-apivideo-org-label-needs-review] | Pull requests which need code review, and approval from maintainers or api.video team. |
197 | | `under-review` | [search][search-apivideo-org-label-under-review] | Pull requests being reviewed by maintainers or api.video team. |
198 | | `requires-changes` | [search][search-apivideo-org-label-requires-changes] | Pull requests which need to be updated based on review comments and then reviewed again. |
199 | | `needs-testing` | [search][search-apivideo-org-label-needs-testing] | Pull requests which need manual testing. |
200 |
201 | [search-apivideo-org-label-enhancement]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Aenhancement
202 | [search-apivideo-org-label-bug]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Abug
203 | [search-apivideo-org-label-question]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Aquestion
204 | [search-apivideo-org-label-feedback]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Afeedback
205 | [search-apivideo-org-label-help-wanted]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Ahelp-wanted
206 | [search-apivideo-org-label-more-information-needed]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Amore-information-needed
207 | [search-apivideo-org-label-needs-reproduction]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Aneeds-reproduction
208 | [search-apivideo-org-label-windows]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Awindows
209 | [search-apivideo-org-label-linux]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Alinux
210 | [search-apivideo-org-label-mac]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Amac
211 | [search-apivideo-org-label-documentation]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Adocumentation
212 | [search-apivideo-org-label-performance]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Aperformance
213 | [search-apivideo-org-label-security]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Asecurity
214 | [search-apivideo-org-label-ui]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Aui
215 | [search-apivideo-org-label-api]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Aapi
216 | [search-apivideo-org-label-blocked]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Ablocked
217 | [search-apivideo-org-label-duplicate]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Aduplicate
218 | [search-apivideo-org-label-wontfix]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Awontfix
219 | [search-apivideo-org-label-invalid]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Ainvalid
220 | [search-apivideo-org-label-package-idea]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Apackage-idea
221 | [search-apivideo-org-label-wrong-repo]: https://github.com/search?q=is%3Aopen+is%3Aissue+user%3Aapivideo+label%3Awrong-repo
222 | [search-apivideo-org-label-work-in-progress]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aapivideo%2Fapivideo+label%3Awork-in-progress
223 | [search-apivideo-org-label-needs-review]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aapivideo%2Fapivideo+label%3Aneeds-review
224 | [search-apivideo-org-label-under-review]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aapivideo%2Fapivideo+label%3Aunder-review
225 | [search-apivideo-org-label-requires-changes]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aapivideo%2Fapivideo+label%3Arequires-changes
226 | [search-apivideo-org-label-needs-testing]: https://github.com/search?q=is%3Aopen+is%3Apr+repo%3Aapivideo%2Fapivideo+label%3Aneeds-testing
227 |
228 | [help-wanted]:https://github.com/search?q=is%3Aopen+is%3Aissue+label%3Ahelp-wanted+user%3Aapivideo+sort%3Acomments-desc
229 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 api.video
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | [](https://twitter.com/intent/follow?screen_name=api_video)
3 | [](https://github.com/apivideo/api.video-android-live-stream)
4 | [](https://community.api.video)
5 | 
6 |
Android RTMP live stream client
7 |
8 | [api.video](https://api.video) is the video infrastructure for product builders. Lightning fast
9 | video APIs for integrating, scaling, and managing on-demand & low latency live streaming features in
10 | your app.
11 |
12 | ## Table of contents
13 |
14 | - [Table of contents](#table-of-contents)
15 | - [Project description](#project-description)
16 | - [Getting started](#getting-started)
17 | - [Installation](#installation)
18 | - [Gradle](#gradle)
19 | - [Permissions](#permissions)
20 | - [Code sample](#code-sample)
21 | - [Tips](#tips)
22 | - [Documentation](#documentation)
23 | - [Dependencies](#dependencies)
24 | - [Sample application](#sample-application)
25 | - [FAQ](#faq)
26 |
27 |
28 |
40 | ## Project description
41 |
42 | This library is an easy way to broadcast livestream to api.video platform on Android.
43 |
44 | ## Getting started
45 |
46 | ### Installation
47 |
48 | #### Gradle
49 |
50 | On build.gradle add the following code in dependencies:
51 |
52 | ```groovy
53 | dependencies {
54 | implementation 'video.api:android-live-stream:1.4.3'
55 | }
56 | ```
57 |
58 | ### Permissions
59 |
60 | ```xml
61 |
62 |
63 |
64 |
65 |
66 |
67 | ```
68 |
69 | Your application must dynamically require `android.permission.CAMERA`
70 | and `android.permission.RECORD_AUDIO`.
71 |
72 | ### Code sample
73 |
74 | 1. Add [permissions](#permissions) to your `AndroidManifest.xml` and request them in your
75 | Activity/Fragment.
76 | 2. Add a `ApiVideoView` to your Activity/Fragment layout for the camera preview.
77 |
78 | ```xml
79 |
80 |
84 | ```
85 |
86 | 3. Implement a `IConnectionListener`.
87 |
88 | ```kotlin
89 | val connectionListener = object : IConnectionListener {
90 | override fun onConnectionSuccess() {
91 | //Add your code here
92 | }
93 |
94 | override fun onConnectionFailed(reason: String?) {
95 | //Add your code here
96 | }
97 |
98 | override fun onDisconnect() {
99 | //Add your code here
100 | }
101 | }
102 | ```
103 |
104 | 4. Create an `ApiVideoLiveStream` instance.
105 |
106 | ```kotlin
107 | class MyFragment : Fragment(), IConnectionListener {
108 | private var apiVideoView: ApiVideoView? = null
109 | private lateinit var apiVideo: ApiVideoLiveStream
110 |
111 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
112 | super.onViewCreated(view, savedInstanceState)
113 |
114 | val apiVideoView = view.findViewById(R.id.apiVideoView)
115 | val audioConfig = AudioConfig(
116 | bitrate = 128 * 1000, // 128 kbps
117 | sampleRate = 44100, // 44.1 kHz
118 | stereo = true,
119 | echoCanceler = true,
120 | noiseSuppressor = true
121 | )
122 | val videoConfig = VideoConfig(
123 | bitrate = 2 * 1000 * 1000, // 2 Mbps
124 | resolution = Resolution.RESOLUTION_720,
125 | fps = 30
126 | )
127 | apiVideo =
128 | ApiVideoLiveStream(
129 | context = getContext(),
130 | connectionListener = this,
131 | initialAudioConfig = audioConfig,
132 | initialVideoConfig = videoConfig,
133 | apiVideoView = apiVideoView
134 | )
135 | }
136 | }
137 | ```
138 |
139 | 5. Start your stream with `startStreaming` method
140 |
141 | For detailed information on this livestream library API, refers
142 | to [API documentation](https://apivideo.github.io/api.video-android-live-stream/).
143 |
144 | ## Tips
145 |
146 | You can check device supported configurations by using the helper: `Helper`
147 |
148 | ## Documentation
149 |
150 | * [API documentation](https://apivideo.github.io/api.video-android-live-stream/)
151 | * [api.video documentation](https://docs.api.video/)
152 |
153 | ## Dependencies
154 |
155 | We are using external library
156 |
157 | | Plugin | README |
158 | | ------ | ------ |
159 | | [StreamPack](https://github.com/ThibaultBee/StreamPack) | [README.md](https://github.com/ThibaultBee/StreamPack/blob/master/README.md) |
160 |
161 | ## Sample application
162 |
163 | A demo application demonstrates how to use this livestream library. See `/example` folder.
164 |
165 | ## FAQ
166 |
167 | If you have any questions, ask us here: https://community.api.video . Or use [Issues].
168 |
169 |
170 | [//]: # (These are reference links used in the body of this note and get stripped out when the markdown processor does its job. There is no need to format nicely because it shouldn't be seen. Thanks SO - http://stackoverflow.com/questions/4823468/store-comments-in-markdown-syntax)
171 |
172 | [Issues]:
173 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | ext {
4 | // SDK and tools
5 | minSdk = 21
6 | compileSdk = 34
7 | targetSdk = 34
8 |
9 | // Kotlin
10 | kotlinVersion = "2.0.20"
11 | dokkaVersion = "1.9.10"
12 |
13 | // Example
14 | versionCode = properties['VERSION_CODE'].toInteger()
15 | versionName = "${properties['VERSION_NAME']}"
16 |
17 | // StreamPack
18 | streamPackVersion = "2.6.1"
19 | }
20 | }
21 | plugins {
22 | id 'com.android.application' version '8.6.1' apply false
23 | id 'com.android.library' version '8.6.1' apply false
24 | id 'org.jetbrains.kotlin.android' version "$kotlinVersion" apply false
25 | id 'org.jetbrains.kotlin.kapt' version "$kotlinVersion" apply false
26 | id 'org.jetbrains.dokka' version "$dokkaVersion" apply false
27 | }
28 |
29 | tasks.register('clean', Delete) {
30 | delete rootProject.buildDir
31 | }
--------------------------------------------------------------------------------
/docs/docs_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/logo-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/example/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | id 'kotlin-kapt'
5 | }
6 |
7 | android {
8 | defaultConfig {
9 | applicationId "video.api.livestream.example"
10 |
11 | minSdk rootProject.minSdk
12 | targetSdk rootProject.targetSdk
13 | compileSdk rootProject.compileSdk
14 |
15 | versionCode rootProject.versionCode
16 | versionName rootProject.versionName
17 |
18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
19 | }
20 |
21 | buildTypes {
22 | release {
23 | minifyEnabled false
24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
25 | }
26 | }
27 | compileOptions {
28 | sourceCompatibility JavaVersion.VERSION_1_8
29 | targetCompatibility JavaVersion.VERSION_1_8
30 | }
31 | kotlinOptions {
32 | jvmTarget = '1.8'
33 | }
34 | buildFeatures {
35 | dataBinding true
36 | viewBinding true
37 | }
38 | namespace 'video.api.livestream.example'
39 | }
40 |
41 | dependencies {
42 | implementation 'androidx.core:core-ktx:1.13.1'
43 | implementation 'androidx.appcompat:appcompat:1.7.0'
44 | implementation 'com.google.android.material:material:1.12.0'
45 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
46 | implementation 'androidx.navigation:navigation-fragment-ktx:2.8.1'
47 | implementation 'androidx.navigation:navigation-ui-ktx:2.8.1'
48 | implementation 'androidx.preference:preference-ktx:1.2.1'
49 |
50 | implementation project(":livestream")
51 |
52 | testImplementation 'junit:junit:4.13.2'
53 | androidTestImplementation 'androidx.test.ext:junit:1.2.1'
54 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
55 | }
--------------------------------------------------------------------------------
/example/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/example/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/example/src/main/java/video/api/livestream/example/ui/main/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package video.api.livestream.example.ui.main
2 |
3 | import android.Manifest
4 | import android.content.Intent
5 | import android.content.pm.PackageManager
6 | import android.os.Bundle
7 | import android.view.Menu
8 | import android.view.MenuItem
9 | import androidx.activity.result.contract.ActivityResultContracts
10 | import androidx.appcompat.app.AppCompatActivity
11 | import androidx.core.app.ActivityCompat
12 | import androidx.core.content.ContextCompat
13 | import video.api.livestream.example.R
14 | import video.api.livestream.example.databinding.ActivityMainBinding
15 | import video.api.livestream.example.ui.preferences.PreferencesActivity
16 | import video.api.livestream.example.ui.utils.DialogHelper
17 |
18 | class MainActivity : AppCompatActivity() {
19 | private val binding: ActivityMainBinding by lazy {
20 | ActivityMainBinding.inflate(layoutInflater)
21 | }
22 |
23 | override fun onCreate(savedInstanceState: Bundle?) {
24 | super.onCreate(savedInstanceState)
25 |
26 | setContentView(binding.root)
27 | setSupportActionBar(binding.toolbar)
28 | }
29 |
30 | override fun onStart() {
31 | super.onStart()
32 |
33 | when {
34 | (ContextCompat.checkSelfPermission(
35 | this,
36 | Manifest.permission.CAMERA
37 | ) == PackageManager.PERMISSION_GRANTED) && (ContextCompat.checkSelfPermission(
38 | this,
39 | Manifest.permission.RECORD_AUDIO
40 | ) == PackageManager.PERMISSION_GRANTED) -> {
41 | launchFragment()
42 | }
43 |
44 | ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)
45 | || ActivityCompat.shouldShowRequestPermissionRationale(
46 | this,
47 | Manifest.permission.RECORD_AUDIO
48 | ) -> {
49 | DialogHelper.showAlertDialog(
50 | this,
51 | getString(R.string.permissions),
52 | getString(R.string.permission_not_granted)
53 | )
54 | requestPermissionLauncher.launch(
55 | arrayOf(
56 | Manifest.permission.CAMERA,
57 | Manifest.permission.RECORD_AUDIO
58 | )
59 | )
60 | }
61 |
62 | else -> {
63 | requestPermissionLauncher.launch(
64 | arrayOf(
65 | Manifest.permission.CAMERA,
66 | Manifest.permission.RECORD_AUDIO
67 | )
68 | )
69 | }
70 | }
71 | }
72 |
73 | private val requestPermissionLauncher =
74 | registerForActivityResult(
75 | ActivityResultContracts.RequestMultiplePermissions()
76 | ) { permissions ->
77 | if ((permissions[Manifest.permission.CAMERA] == true)
78 | && (permissions[Manifest.permission.RECORD_AUDIO] == true)
79 | ) {
80 | launchFragment()
81 | } else {
82 | showPermissionErrorAndFinish()
83 | }
84 | }
85 |
86 | private fun launchFragment() {
87 | supportFragmentManager.beginTransaction()
88 | .replace(R.id.container, PreviewFragment())
89 | .commitNow()
90 | }
91 |
92 | override fun onCreateOptionsMenu(menu: Menu): Boolean {
93 | menuInflater.inflate(R.menu.menu_main, menu)
94 | return true
95 | }
96 |
97 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
98 | return when (item.itemId) {
99 | R.id.action_settings -> {
100 | goToPreferencesActivity()
101 | true
102 | }
103 |
104 | else -> super.onOptionsItemSelected(item)
105 | }
106 | }
107 |
108 | private fun goToPreferencesActivity() {
109 | val intent = Intent(this, PreferencesActivity::class.java)
110 | startActivity(intent)
111 | }
112 |
113 | private fun showPermissionErrorAndFinish() {
114 | DialogHelper.showPermissionAlertDialog(this) { this.finish() }
115 | }
116 | }
--------------------------------------------------------------------------------
/example/src/main/java/video/api/livestream/example/ui/main/PreviewFragment.kt:
--------------------------------------------------------------------------------
1 | package video.api.livestream.example.ui.main
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.pm.ActivityInfo
5 | import android.os.Bundle
6 | import android.view.LayoutInflater
7 | import android.view.ScaleGestureDetector
8 | import android.view.View
9 | import android.view.ViewGroup
10 | import android.widget.Toast
11 | import androidx.fragment.app.Fragment
12 | import androidx.fragment.app.viewModels
13 | import video.api.livestream.example.R
14 | import video.api.livestream.example.databinding.FragmentPreviewBinding
15 | import video.api.livestream.example.ui.utils.DialogHelper
16 |
17 | class PreviewFragment : Fragment() {
18 | private val viewModel: PreviewViewModel by viewModels()
19 | private lateinit var binding: FragmentPreviewBinding
20 |
21 | /**
22 | * Zooming gesture
23 | *
24 | * scaleFactor > 1 == Zooming in
25 | * scaleFactor < 1 == Zooming out
26 | *
27 | * scaleFactor will start at a value of 1 when the gesture is begun.
28 | * Then its value will persist until the gesture has ended.
29 | * If we save the zoomRatio in savedScale when the gesture has begun,
30 | * we can easily add a relative scale to the zoom.
31 | *
32 | * If we are zooming out, the scale is between 0-1.
33 | * Meaning we can use this as a percentage from the savedScale
34 | *
35 | * Zooming in is linear zoom
36 | * Zooming out is percentage zoom between 1f & savedScale
37 | */
38 | private val pinchGesture: ScaleGestureDetector by lazy {
39 | ScaleGestureDetector(
40 | binding.apiVideoView.context,
41 | object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
42 | private var savedZoomRatio: Float = 1f
43 | override fun onScale(detector: ScaleGestureDetector): Boolean {
44 | viewModel.zoomRatio = if (detector.scaleFactor < 1) {
45 | savedZoomRatio * detector.scaleFactor
46 | } else {
47 | savedZoomRatio + ((detector.scaleFactor - 1))
48 | }
49 | return super.onScale(detector)
50 | }
51 |
52 | override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
53 | detector.currentSpan
54 | savedZoomRatio = viewModel.zoomRatio
55 | return super.onScaleBegin(detector)
56 | }
57 | })
58 | }
59 |
60 | override fun onCreateView(
61 | inflater: LayoutInflater,
62 | container: ViewGroup?,
63 | savedInstanceState: Bundle?
64 | ): View {
65 | binding = FragmentPreviewBinding.inflate(inflater, container, false)
66 | return binding.root
67 | }
68 |
69 | @SuppressLint("MissingPermission", "ClickableViewAccessibility")
70 | override fun onResume() {
71 | super.onResume()
72 |
73 | // Listen to touch for zoom
74 | binding.apiVideoView.setOnTouchListener { _, event ->
75 | pinchGesture.onTouchEvent(event)
76 | }
77 |
78 | viewModel.buildLiveStream(binding.apiVideoView)
79 | binding.liveButton.setOnCheckedChangeListener { _, isChecked ->
80 | if (isChecked) {
81 | /**
82 | * Lock orientation in live to avoid stream interruption if
83 | * user turns the device.
84 | */
85 | requireActivity().requestedOrientation =
86 | ActivityInfo.SCREEN_ORIENTATION_LOCKED
87 | viewModel.startStream()
88 | } else {
89 | viewModel.stopStream()
90 | requireActivity().requestedOrientation =
91 | ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
92 | }
93 | }
94 |
95 | binding.switchButton.setOnClickListener {
96 | viewModel.switchCamera()
97 | }
98 |
99 | binding.muteButton.setOnClickListener {
100 | viewModel.toggleMute()
101 | }
102 |
103 | viewModel.onError.observe(viewLifecycleOwner) {
104 | binding.liveButton.isChecked = false
105 | manageError(getString(R.string.error), it)
106 | }
107 |
108 | viewModel.onDisconnect.observe(viewLifecycleOwner) {
109 | binding.liveButton.isChecked = false
110 | showDisconnection()
111 | }
112 | }
113 |
114 | private fun manageError(title: String, message: String) {
115 | requireActivity().requestedOrientation =
116 | ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
117 | DialogHelper.showAlertDialog(requireContext(), title, message)
118 | }
119 |
120 | private fun showDisconnection() {
121 | Toast.makeText(requireContext(), getString(R.string.disconnection), Toast.LENGTH_SHORT)
122 | .show()
123 | }
124 | }
--------------------------------------------------------------------------------
/example/src/main/java/video/api/livestream/example/ui/main/PreviewViewModel.kt:
--------------------------------------------------------------------------------
1 | package video.api.livestream.example.ui.main
2 |
3 | import android.Manifest
4 | import android.app.Application
5 | import androidx.annotation.RequiresPermission
6 | import androidx.lifecycle.AndroidViewModel
7 | import androidx.lifecycle.MutableLiveData
8 | import video.api.livestream.ApiVideoLiveStream
9 | import video.api.livestream.enums.CameraFacingDirection
10 | import video.api.livestream.example.ui.utils.Configuration
11 | import video.api.livestream.interfaces.IConnectionListener
12 | import video.api.livestream.models.AudioConfig
13 | import video.api.livestream.models.VideoConfig
14 | import video.api.livestream.views.ApiVideoView
15 |
16 | class PreviewViewModel(application: Application) : AndroidViewModel(application),
17 | IConnectionListener {
18 | private lateinit var liveStream: ApiVideoLiveStream
19 | private val configuration = Configuration(getApplication())
20 |
21 | val onError = MutableLiveData()
22 | val onDisconnect = MutableLiveData()
23 |
24 | var zoomRatio: Float
25 | get() = liveStream.zoomRatio
26 | set(value) {
27 | liveStream.zoomRatio = value
28 | }
29 |
30 | @RequiresPermission(allOf = [Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA])
31 | fun buildLiveStream(apiVideoView: ApiVideoView) {
32 | val audioConfig = AudioConfig(
33 | bitrate = configuration.audio.bitrate,
34 | sampleRate = configuration.audio.sampleRate,
35 | stereo = configuration.audio.numberOfChannels == 2,
36 | echoCanceler = configuration.audio.enableEchoCanceler,
37 | noiseSuppressor = configuration.audio.enableEchoCanceler
38 | )
39 | val videoConfig = VideoConfig(
40 | bitrate = configuration.video.bitrate * 1024, // to bps
41 | resolution = configuration.video.resolution,
42 | fps = configuration.video.fps,
43 | )
44 | liveStream =
45 | ApiVideoLiveStream(
46 | context = getApplication(),
47 | apiVideoView = apiVideoView,
48 | connectionListener = this,
49 | initialAudioConfig = audioConfig,
50 | initialVideoConfig = videoConfig
51 | )
52 | }
53 |
54 | fun startStream() {
55 | try {
56 | liveStream.startStreaming(configuration.endpoint.streamKey, configuration.endpoint.url)
57 | } catch (e: Exception) {
58 | onError.postValue(e.message)
59 | }
60 | }
61 |
62 | fun stopStream() {
63 | liveStream.stopStreaming()
64 | }
65 |
66 | override fun onCleared() {
67 | super.onCleared()
68 | liveStream.release()
69 | }
70 |
71 | fun switchCamera() {
72 | if (liveStream.cameraPosition == CameraFacingDirection.BACK) {
73 | liveStream.cameraPosition = CameraFacingDirection.FRONT
74 | } else {
75 | liveStream.cameraPosition = CameraFacingDirection.BACK
76 | }
77 | }
78 |
79 | fun toggleMute() {
80 | liveStream.isMuted = !liveStream.isMuted
81 | }
82 |
83 | override fun onConnectionFailed(reason: String) {
84 | onError.postValue(reason)
85 | }
86 |
87 | override fun onConnectionSuccess() {
88 | }
89 |
90 | override fun onDisconnect() {
91 | onDisconnect.postValue(true)
92 | }
93 | }
--------------------------------------------------------------------------------
/example/src/main/java/video/api/livestream/example/ui/preferences/PreferencesActivity.kt:
--------------------------------------------------------------------------------
1 | package video.api.livestream.example.ui.preferences
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import video.api.livestream.example.R
6 |
7 | class PreferencesActivity : AppCompatActivity() {
8 |
9 | override fun onCreate(savedInstanceState: Bundle?) {
10 | super.onCreate(savedInstanceState)
11 | setContentView(R.layout.activity_preferences)
12 | if (savedInstanceState == null) {
13 | supportFragmentManager
14 | .beginTransaction()
15 | .replace(R.id.preferences, PreferencesFragment())
16 | .commit()
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/example/src/main/java/video/api/livestream/example/ui/preferences/PreferencesFragment.kt:
--------------------------------------------------------------------------------
1 | package video.api.livestream.example.ui.preferences
2 |
3 | import android.os.Bundle
4 | import androidx.preference.ListPreference
5 | import androidx.preference.PreferenceFragmentCompat
6 | import video.api.livestream.ConfigurationHelper
7 | import video.api.livestream.enums.Resolution
8 | import video.api.livestream.example.R
9 |
10 | class PreferencesFragment : PreferenceFragmentCompat() {
11 | override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
12 | setPreferencesFromResource(R.xml.preferences, rootKey)
13 | }
14 |
15 | override fun onResume() {
16 | super.onResume()
17 | inflatesPreferences()
18 | }
19 |
20 | private fun inflatesPreferences() {
21 | (findPreference(getString(R.string.video_resolution_key)) as ListPreference?)!!.apply {
22 | val resolutionsList = Resolution.values().map { it.toString() }.toTypedArray()
23 | entryValues = resolutionsList
24 | entries = resolutionsList
25 | if (value == null) {
26 | value = Resolution.RESOLUTION_720.toString()
27 | }
28 | }
29 |
30 | (findPreference(getString(R.string.video_fps_key)) as ListPreference?)!!.apply {
31 | val supportedFramerates = ConfigurationHelper.video.getSupportedFrameRates(
32 | requireContext(),
33 | ConfigurationHelper.video.getBackCamerasList(requireContext()).first()
34 | )
35 | entryValues.filter { fps ->
36 | supportedFramerates.any { it.contains(fps.toString().toInt()) }
37 | }.toTypedArray().run {
38 | this@apply.entries = this
39 | this@apply.entryValues = this
40 | }
41 | }
42 |
43 | (findPreference(getString(R.string.audio_sample_rate_key)) as ListPreference?)!!.apply {
44 | val supportedSampleRate =
45 | ConfigurationHelper.audio.getSupportedSampleRates()
46 | entries =
47 | supportedSampleRate.map { "${"%.1f".format(it.toString().toFloat() / 1000)} kHz" }
48 | .toTypedArray()
49 | entryValues = supportedSampleRate.map { "$it" }.toTypedArray()
50 | if (entry == null) {
51 | value = "44100"
52 | }
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/example/src/main/java/video/api/livestream/example/ui/utils/Configuration.kt:
--------------------------------------------------------------------------------
1 | package video.api.livestream.example.ui.utils
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import android.content.res.Resources
6 | import android.util.Size
7 | import androidx.preference.PreferenceManager
8 | import video.api.livestream.example.R
9 |
10 | class Configuration(context: Context) {
11 | private val sharedPref = PreferenceManager.getDefaultSharedPreferences(context)
12 | private val resources = context.resources
13 | val video = Video(sharedPref, resources)
14 | val audio = Audio(sharedPref, resources)
15 | val endpoint = Endpoint(sharedPref, resources)
16 |
17 | class Video(private val sharedPref: SharedPreferences, private val resources: Resources) {
18 | var fps: Int = 30
19 | get() = sharedPref.getString(
20 | resources.getString(R.string.video_fps_key),
21 | field.toString()
22 | )!!.toInt()
23 |
24 | var resolution: Size = Size(1280, 720)
25 | get() {
26 | val res = sharedPref.getString(
27 | resources.getString(R.string.video_resolution_key),
28 | field.toString()
29 | )!!
30 | val resArray = res.split("x")
31 | return Size(
32 | resArray[0].toInt(),
33 | resArray[1].toInt()
34 | )
35 | }
36 |
37 | var bitrate: Int = 2000
38 | get() = sharedPref.getInt(
39 | resources.getString(R.string.video_bitrate_key),
40 | field
41 | )
42 | }
43 |
44 | class Audio(private val sharedPref: SharedPreferences, private val resources: Resources) {
45 | var numberOfChannels: Int = 2
46 | get() = sharedPref.getString(
47 | resources.getString(R.string.audio_number_of_channels_key),
48 | field.toString()
49 | )!!.toInt()
50 |
51 | var bitrate: Int = 128000
52 | get() = sharedPref.getString(
53 | resources.getString(R.string.audio_bitrate_key),
54 | field.toString()
55 | )!!.toInt()
56 |
57 | var sampleRate: Int = 48000
58 | get() = sharedPref.getString(
59 | resources.getString(R.string.audio_sample_rate_key),
60 | field.toString()
61 | )!!.toInt()
62 |
63 | var enableEchoCanceler: Boolean = false
64 | get() = sharedPref.getBoolean(
65 | resources.getString(R.string.audio_enable_echo_canceler_key),
66 | field
67 | )
68 |
69 | var enableNoiseSuppressor: Boolean = false
70 | get() = sharedPref.getBoolean(
71 | resources.getString(R.string.audio_enable_noise_suppressor_key),
72 | field
73 | )
74 | }
75 |
76 | class Endpoint(private val sharedPref: SharedPreferences, private val resources: Resources) {
77 | var url: String = ""
78 | get() = sharedPref.getString(
79 | resources.getString(R.string.rtmp_endpoint_url_key),
80 | field
81 | )!!
82 |
83 | var streamKey: String = ""
84 | get() = sharedPref.getString(resources.getString(R.string.rtmp_stream_key_key), field)!!
85 | }
86 | }
--------------------------------------------------------------------------------
/example/src/main/java/video/api/livestream/example/ui/utils/DialogHelper.kt:
--------------------------------------------------------------------------------
1 | package video.api.livestream.example.ui.utils
2 |
3 | import android.content.Context
4 | import android.content.DialogInterface
5 | import androidx.appcompat.app.AlertDialog
6 | import video.api.livestream.example.R
7 |
8 | object DialogHelper {
9 | fun showAlertDialog(context: Context, title: String, message: String = "") {
10 | AlertDialog.Builder(context)
11 | .setTitle(title)
12 | .setMessage(message)
13 | .setPositiveButton(android.R.string.ok) { dialogInterface: DialogInterface, _: Int -> dialogInterface.dismiss() }
14 | .show()
15 | }
16 |
17 | fun showPermissionAlertDialog(context: Context, afterPositiveButton: () -> Unit = {}) {
18 | AlertDialog.Builder(context)
19 | .setTitle(R.string.permissions)
20 | .setMessage(R.string.permission_not_granted)
21 | .setPositiveButton(android.R.string.ok) { dialogInterface: DialogInterface, _: Int ->
22 | dialogInterface.dismiss()
23 | afterPositiveButton()
24 | }
25 | .show()
26 | }
27 | }
--------------------------------------------------------------------------------
/example/src/main/res/drawable/circle_shape.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/example/src/main/res/drawable/circle_toggle_button.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/src/main/res/drawable/circle_toggle_button_off.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
12 |
--------------------------------------------------------------------------------
/example/src/main/res/drawable/circle_toggle_button_on.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/example/src/main/res/drawable/ic_api_video.xml:
--------------------------------------------------------------------------------
1 |
6 |
12 |
18 |
19 |
--------------------------------------------------------------------------------
/example/src/main/res/drawable/ic_baseline_mic_off_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/example/src/main/res/drawable/ic_baseline_switch_camera_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/example/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
20 |
21 |
22 |
23 |
27 |
28 |
--------------------------------------------------------------------------------
/example/src/main/res/layout/activity_preferences.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
9 |
--------------------------------------------------------------------------------
/example/src/main/res/layout/fragment_preview.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
22 |
23 |
33 |
34 |
44 |
45 |
46 |
47 |
60 |
61 |
--------------------------------------------------------------------------------
/example/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/src/main/res/values/arrays.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 24
5 | 25
6 | 30
7 |
8 |
9 |
10 | Mono
11 | Stereo
12 |
13 |
14 |
15 | 1
16 | 2
17 |
18 |
19 |
20 | 24 Kbps
21 | 64 Kbps
22 | 128 Kbps
23 | 192 Kbps
24 |
25 |
26 |
27 | 24000
28 | 64000
29 | 128000
30 | 192000
31 |
32 |
33 |
--------------------------------------------------------------------------------
/example/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FA5B30
4 | #D9E1EC
5 | #FFFFFF
6 | #66FFFFFF
7 |
--------------------------------------------------------------------------------
/example/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | LiveStream
3 |
4 |
5 | Settings
6 |
7 | Switch camera
8 | Mute/unmute microphone
9 | Live!
10 | Stop
11 | Endpoint
12 | Error
13 | You have been disconnected
14 |
15 |
16 | Video
17 | video_resolution_key
18 | Resolution
19 | video_fps_key
20 | Framerate
21 | video_bitrate_key
22 | Bitrate (in Kbps)
23 |
24 | Audio
25 | audio_number_of_channel_key
26 | Number of channels
27 | audio_bitrate_key
28 | audio_sample_rate_key
29 | Sample rate
30 | Enable echo canceler
31 | audio_enable_echo_canceler_key
32 | audio_enable_noise_suppressor_key
33 | Enable noise suppressor
34 | rtmp://broadcast.api.video/s/
35 | rtmp_address_key
36 | RTMP endpoint
37 | rtmp_stream_key_key
38 | Stream key
39 | Permissions
40 | Permissions not granted: leaving!
41 |
--------------------------------------------------------------------------------
/example/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/example/src/main/res/xml/preferences.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
8 |
9 |
16 |
17 |
25 |
26 |
27 |
28 |
29 |
36 |
37 |
44 |
45 |
50 |
51 |
55 |
56 |
60 |
61 |
62 |
63 |
64 |
69 |
70 |
75 |
76 |
--------------------------------------------------------------------------------
/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 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 | # Enables namespacing of each library's R class so that its R class includes only the
23 | # resources declared in the library itself and none from the library's dependencies,
24 | # thereby reducing the size of the R class for that library
25 | android.nonTransitiveRClass=false
26 | android.nonFinalResIds=false
27 |
28 | POM_NAME=android-live-stream
29 | POM_ARTIFACT_ID=android-live-stream
30 | POM_PACKAGING=aar
31 |
32 | VERSION_NAME=1.4.3
33 | VERSION_CODE=1004003
34 | GROUP=video.api
35 |
36 | POM_DESCRIPTION=Android live stream module for api.video service.
37 | POM_URL=https://github.com/apivideo/api.video-android-live-stream
38 | POM_SCM_URL=https://github.com/apivideo/api.video-android-live-stream.git
39 | POM_SCM_CONNECTION=scm:git@github.com:apivideo/api.video-android-live-stream.git
40 | POM_SCM_DEV_CONNECTION=scm:git@github.com:apivideo/api.video-android-live-stream.git
41 | POM_LICENCE_NAME=MIT Licence
42 | POM_LICENCE_URL=https://opensource.org/licenses/mit-license.php
43 | POM_LICENCE_DIST=repo
44 | POM_DEVELOPER_ID=api.video
45 | POM_DEVELOPER_NAME=Ecosystem Team
46 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apivideo/api.video-android-live-stream/97a25101ee8fbdeeac4c72abdf5d7d67b2b41ad0/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Oct 13 16:06:55 CEST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/livestream/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/livestream/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'org.jetbrains.dokka'
4 | apply from: 'maven-push.gradle'
5 |
6 |
7 | android {
8 | defaultConfig {
9 | minSdk rootProject.minSdk
10 | targetSdk rootProject.targetSdk
11 | compileSdk rootProject.compileSdk
12 |
13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
14 | consumerProguardFiles "consumer-rules.pro"
15 | }
16 |
17 | buildTypes {
18 | release {
19 | minifyEnabled false
20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
21 | }
22 | }
23 | compileOptions {
24 | sourceCompatibility JavaVersion.VERSION_1_8
25 | targetCompatibility JavaVersion.VERSION_1_8
26 | }
27 | kotlinOptions {
28 | jvmTarget = '1.8'
29 | }
30 | namespace 'video.api.livestream'
31 | }
32 |
33 | dokkaHtml {
34 | moduleName.set("api.video Android $project.name library")
35 | suppressInheritedMembers.set(true)
36 |
37 | dokkaSourceSets {
38 | named("main") {
39 | noAndroidSdkLink.set(false)
40 | skipDeprecated.set(true)
41 | includeNonPublic.set(false)
42 | skipEmptyPackages.set(true)
43 | }
44 | }
45 | pluginsMapConfiguration.set(
46 | ["org.jetbrains.dokka.base.DokkaBase": """{
47 | "customAssets" : [
48 | "${file("$rootDir/docs/docs_logo.svg")}",
49 | "${file("$rootDir/docs/logo-icon.svg")}"
50 | ]
51 | }"""]
52 | )
53 | }
54 |
55 | dependencies {
56 | implementation 'androidx.core:core-ktx:1.13.1'
57 | implementation 'androidx.appcompat:appcompat:1.7.0'
58 | implementation "io.github.thibaultbee:streampack:$streamPackVersion"
59 | implementation "io.github.thibaultbee:streampack-extension-rtmp:$streamPackVersion"
60 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
61 |
62 | testImplementation 'junit:junit:4.13.2'
63 | androidTestImplementation 'androidx.test.ext:junit:1.2.1'
64 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
65 | }
--------------------------------------------------------------------------------
/livestream/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/apivideo/api.video-android-live-stream/97a25101ee8fbdeeac4c72abdf5d7d67b2b41ad0/livestream/consumer-rules.pro
--------------------------------------------------------------------------------
/livestream/maven-push.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Chris Banes
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 | apply plugin: 'maven-publish'
18 | apply plugin: 'signing'
19 |
20 | def isReleaseBuild() {
21 | return !VERSION_NAME.contains("SNAPSHOT")
22 | }
23 |
24 | def getReleaseRepositoryUrl() {
25 | return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL
26 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
27 | }
28 |
29 | def getSnapshotRepositoryUrl() {
30 | return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL
31 | : "https://oss.sonatype.org/content/repositories/snapshots/"
32 | }
33 |
34 | def getRepositoryUsername() {
35 | return hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : System.getenv("NEXUS_USERNAME")
36 | }
37 |
38 | def getRepositoryPassword() {
39 | return hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : System.getenv("NEXUS_PASSWORD")
40 | }
41 |
42 | afterEvaluate { project ->
43 | publishing {
44 | publications {
45 | release(MavenPublication) {
46 | from components.release
47 |
48 | groupId = GROUP
49 | artifactId = POM_ARTIFACT_ID
50 | version = VERSION_NAME
51 |
52 | pom {
53 | name = POM_NAME
54 | packaging = POM_PACKAGING
55 | description = POM_DESCRIPTION
56 | url = POM_URL
57 |
58 |
59 | scm {
60 | url = POM_SCM_URL
61 | connection = POM_SCM_CONNECTION
62 | developerConnection = POM_SCM_DEV_CONNECTION
63 | }
64 |
65 | licenses {
66 | license {
67 | name = POM_LICENCE_NAME
68 | url = POM_LICENCE_URL
69 | distribution = POM_LICENCE_DIST
70 | }
71 | }
72 |
73 | developers {
74 | developer {
75 | id = POM_DEVELOPER_ID
76 | name = POM_DEVELOPER_NAME
77 | }
78 | }
79 | }
80 | }
81 | }
82 | repositories {
83 | maven {
84 | url = isReleaseBuild() ? getReleaseRepositoryUrl() : getSnapshotRepositoryUrl()
85 |
86 | credentials {
87 | username = getRepositoryUsername()
88 | password = getRepositoryPassword()
89 | }
90 | }
91 | }
92 | }
93 |
94 | signing {
95 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
96 | sign publishing.publications.release
97 | }
98 | }
--------------------------------------------------------------------------------
/livestream/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/livestream/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/livestream/src/main/java/video/api/livestream/ApiVideoLiveStream.kt:
--------------------------------------------------------------------------------
1 | package video.api.livestream
2 |
3 | import android.Manifest
4 | import android.content.Context
5 | import android.util.Log
6 | import androidx.annotation.RequiresPermission
7 | import io.github.thibaultbee.streampack.error.StreamPackError
8 | import io.github.thibaultbee.streampack.ext.rtmp.streamers.CameraRtmpLiveStreamer
9 | import io.github.thibaultbee.streampack.listeners.OnConnectionListener
10 | import io.github.thibaultbee.streampack.listeners.OnErrorListener
11 | import io.github.thibaultbee.streampack.utils.*
12 | import kotlinx.coroutines.*
13 | import video.api.livestream.enums.CameraFacingDirection
14 | import video.api.livestream.interfaces.IConnectionListener
15 | import video.api.livestream.models.AudioConfig
16 | import video.api.livestream.models.VideoConfig
17 | import video.api.livestream.views.ApiVideoView
18 |
19 |
20 | /**
21 | * @param context application context
22 | * @param apiVideoView where to display preview. Could be null if you don't have a preview.
23 | * @param connectionListener connection callbacks
24 | * @param initialAudioConfig initial audio configuration. Could be change later with [audioConfig] field.
25 | * @param initialVideoConfig initial video configuration. Could be change later with [videoConfig] field.
26 | * @param initialCameraPosition initial camera. Could be change later with [cameraPosition] field.
27 | * @param permissionRequester permission requester. Called when permissions are required. Always call [onGranted] when permissions are granted.
28 | */
29 | @RequiresPermission(allOf = [Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA])
30 | fun ApiVideoLiveStream(
31 | context: Context,
32 | apiVideoView: ApiVideoView,
33 | connectionListener: IConnectionListener,
34 | initialAudioConfig: AudioConfig? = null,
35 | initialVideoConfig: VideoConfig? = null,
36 | initialCameraPosition: CameraFacingDirection = CameraFacingDirection.BACK,
37 | permissionRequester: (List, onGranted: () -> Unit) -> Unit = { _, onGranted -> onGranted() }
38 | ): ApiVideoLiveStream {
39 | return ApiVideoLiveStream(
40 | context,
41 | apiVideoView,
42 | connectionListener,
43 | permissionRequester
44 | ).apply {
45 | audioConfig = initialAudioConfig
46 | videoConfig = initialVideoConfig
47 | cameraPosition = initialCameraPosition
48 | }
49 | }
50 |
51 | /**
52 | * Manages both livestream and camera preview.
53 | */
54 | class ApiVideoLiveStream
55 | /**
56 | * @param context application context
57 | * @param apiVideoView where to display preview. Could be null if you don't have a preview.
58 | * @param connectionListener connection callbacks
59 | * @param permissionRequester permission requester. Called when permissions are required. Always call [onGranted] when permissions are granted.
60 | */
61 | @RequiresPermission(allOf = [Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA])
62 | constructor(
63 | private val context: Context,
64 | private val apiVideoView: ApiVideoView,
65 | private val connectionListener: IConnectionListener,
66 | private val permissionRequester: (List, onGranted: () -> Unit) -> Unit = { _, onGranted -> onGranted() }
67 | ) {
68 | companion object {
69 | private const val TAG = "ApiVideoLiveStream"
70 | }
71 |
72 | private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
73 |
74 | /**
75 | * Sets/gets audio configuration once you have created the a [ApiVideoLiveStream] instance.
76 | */
77 | var audioConfig: AudioConfig? = null
78 | @RequiresPermission(Manifest.permission.RECORD_AUDIO)
79 | set(value) {
80 | require(value != null) { "Audio config must not be null" }
81 | if (isStreaming) {
82 | throw UnsupportedOperationException("You have to stop streaming first")
83 | }
84 | permissionRequester(
85 | listOf(
86 | Manifest.permission.RECORD_AUDIO,
87 | )
88 | ) {
89 | streamer.configure(value.toSdkConfig())
90 | }
91 |
92 | field = value
93 | }
94 |
95 | /**
96 | * Sets/gets video configuration once you have created the a [ApiVideoLiveStream] instance.
97 | */
98 | var videoConfig: VideoConfig? = null
99 | /**
100 | * Sets the new video configuration.
101 | * It will restart preview if resolution has been changed.
102 | * Encoders settings will be applied in next [startStreaming].
103 | *
104 | * @param value new video configuration
105 | */
106 | @RequiresPermission(Manifest.permission.CAMERA)
107 | set(value) {
108 | require(value != null) { "Audio config must not be null" }
109 | if (isStreaming) {
110 | throw UnsupportedOperationException("You have to stop streaming first")
111 | }
112 |
113 | val mustRestartPreview = if (videoConfig?.fps != value.fps) {
114 | Log.i(
115 | TAG,
116 | "Frame rate has been changed from ${videoConfig?.fps} to ${value.fps}. Restarting preview."
117 | )
118 | true
119 | } else {
120 | false
121 | }
122 |
123 | if (mustRestartPreview) {
124 | stopPreview()
125 | }
126 |
127 | streamer.configure(value.toSdkConfig())
128 | field = value
129 |
130 | if (mustRestartPreview) {
131 | try {
132 | startPreview()
133 | } catch (e: UnsupportedOperationException) {
134 | Log.i(TAG, "Can't start preview: ${e.message}")
135 | }
136 | }
137 | }
138 |
139 | private val internalConnectionListener = object : OnConnectionListener {
140 | override fun onFailed(message: String) {
141 | connectionListener.onConnectionFailed(message)
142 | }
143 |
144 | override fun onLost(message: String) {
145 | connectionListener.onDisconnect()
146 | }
147 |
148 | override fun onSuccess() {
149 | connectionListener.onConnectionSuccess()
150 | }
151 | }
152 |
153 | private val errorListener = object : OnErrorListener {
154 | override fun onError(error: StreamPackError) {
155 | _isStreaming = false
156 | Log.e(TAG, "An error happened", error)
157 | }
158 | }
159 |
160 | private val streamer = CameraRtmpLiveStreamer(
161 | context = context,
162 | enableAudio = true,
163 | initialOnErrorListener = errorListener,
164 | initialOnConnectionListener = internalConnectionListener
165 | )
166 |
167 | /**
168 | * Get/set video bitrate during a streaming in bps.
169 | * Value will be reset to provided [VideoConfig.startBitrate] for a new stream.
170 | */
171 | var videoBitrate: Int
172 | /**
173 | * Get video bitrate.
174 | *
175 | * @return video bitrate in bps
176 | */
177 | get() = streamer.settings.video.bitrate
178 | /**
179 | * Set video bitrate.
180 | *
181 | * @param value video bitrate in bps
182 | */
183 | set(value) {
184 | streamer.settings.video.bitrate = value
185 | }
186 |
187 | /**
188 | * Get/set current camera facing direction.
189 | *
190 | * @see [camera]
191 | */
192 | var cameraPosition: CameraFacingDirection
193 | /**
194 | * Get current camera facing direction.
195 | *
196 | * @return facing direction of the current camera
197 | */
198 | get() = CameraFacingDirection.fromCameraId(context, streamer.camera)
199 | /**
200 | * Set current camera facing direction.
201 | *
202 | * @param value camera facing direction
203 | */
204 | set(value) {
205 | if (((value == CameraFacingDirection.BACK) && (context.isFrontCamera(streamer.camera)))
206 | || ((value == CameraFacingDirection.FRONT) && (context.isBackCamera(streamer.camera)))
207 | ) {
208 | permissionRequester(
209 | listOf(
210 | Manifest.permission.CAMERA,
211 | )
212 | ) {
213 | streamer.camera = value.toCameraId(context)
214 | }
215 | }
216 | }
217 |
218 | /**
219 | * Get/set current camera.
220 | *
221 | * @see [cameraPosition]
222 | */
223 | var camera: String
224 | /**
225 | * Gets current camera.
226 | * It is often like "0" for back camera and "1" for front camera.
227 | *
228 | * @return the current camera
229 | */
230 | get() = streamer.camera
231 | /**
232 | * Sets current camera.
233 | *
234 | * @param value the current camera
235 | */
236 | set(value) {
237 | streamer.camera = value
238 | }
239 |
240 | init {
241 | try {
242 | apiVideoView.streamer = streamer
243 | } catch (e: Exception) {
244 | Log.w(TAG, "Can't set streamer to ApiVideoView: ${e.message}")
245 | }
246 | }
247 |
248 | /**
249 | * Mute/Unmute microphone
250 | */
251 | var isMuted: Boolean
252 | /**
253 | * Get mute value.
254 | *
255 | * @return [Boolean.true] if audio is muted, [Boolean.false] if audio is not muted.
256 | */
257 | get() = streamer.settings.audio.isMuted
258 | /**
259 | * Set mute value.
260 | *
261 | * @param value [Boolean.true] to mute audio, [Boolean.false] to unmute audio.
262 | */
263 | set(value) {
264 | streamer.settings.audio.isMuted = value
265 | }
266 |
267 |
268 | /**
269 | * Set/get the zoom ratio.
270 | */
271 | var zoomRatio: Float
272 | /**
273 | * Get the zoom ratio.
274 | *
275 | * @return the zoom ratio
276 | */
277 | get() = streamer.settings.camera.zoom.zoomRatio
278 | /**
279 | * Set the zoom ratio.
280 | *
281 | * @param value the zoom ratio
282 | */
283 | set(value) {
284 | streamer.settings.camera.zoom.zoomRatio = value
285 | }
286 |
287 | /**
288 | * Start a new RTMP stream.
289 | *
290 | * @param streamKey RTMP stream key. For security purpose, you must not expose it.
291 | * @param url RTMP Url. Default value (not set or null) is api.video RTMP broadcast url.
292 | * @see [stopStreaming]
293 | */
294 | fun startStreaming(
295 | streamKey: String,
296 | url: String = context.getString(R.string.default_rtmp_url),
297 | ) {
298 | require(!isStreaming) { "Stream is already running" }
299 | require(streamKey.isNotEmpty()) { "Stream key must not be empty" }
300 | require(url.isNotEmpty()) { "Url must not be empty" }
301 | require(audioConfig != null) { "Audio config must be set" }
302 | require(videoConfig != null) { "Video config must be set" }
303 |
304 | scope.launch {
305 | withContext(context = Dispatchers.IO) {
306 | try {
307 | streamer.connect(url.addTrailingSlashIfNeeded() + streamKey)
308 | try {
309 | streamer.startStream()
310 | _isStreaming = true
311 | } catch (e: Exception) {
312 | streamer.disconnect()
313 | connectionListener.onConnectionFailed("$e")
314 | throw e
315 | }
316 | } catch (e: Exception) {
317 | Log.e(TAG, "Failed to start stream", e)
318 | }
319 | }
320 | }
321 | }
322 |
323 | /**
324 | * Stops running stream.
325 | *
326 | * @see [startStreaming]
327 | */
328 | fun stopStreaming() {
329 | val isConnected = streamer.isConnected
330 | scope.launch {
331 | withContext(context = Dispatchers.IO) {
332 | streamer.stopStream()
333 | streamer.disconnect()
334 | if (isConnected) {
335 | connectionListener.onDisconnect()
336 | }
337 | _isStreaming = false
338 | }
339 | }
340 | }
341 |
342 |
343 | /**
344 | * Hack for private setter of [isStreaming].
345 | */
346 | private var _isStreaming: Boolean = false
347 |
348 | /**
349 | * Check the streaming state.
350 | *
351 | * @return true if you are streaming, false otherwise
352 | * @see [startStreaming]
353 | * @see [stopStreaming]
354 | */
355 | val isStreaming: Boolean
356 | get() = _isStreaming
357 |
358 | /**
359 | * Starts camera preview of [cameraPosition].
360 | *
361 | * The surface provided in the constructor already manages [startPreview] and [stopPreview].
362 | * Use this method only if you need to explicitly start preview.
363 | *
364 | * @see [stopPreview]
365 | */
366 | @RequiresPermission(Manifest.permission.CAMERA)
367 | fun startPreview() {
368 | permissionRequester(
369 | listOf(
370 | Manifest.permission.CAMERA,
371 | )
372 | ) {
373 | if (videoConfig == null) {
374 | Log.w(TAG, "Video config is not set")
375 | return@permissionRequester
376 | }
377 | try {
378 | apiVideoView.startPreview()
379 | } catch (t: Throwable) {
380 | Log.e(TAG, "Can't start preview: ${t.message}")
381 | }
382 | }
383 | }
384 |
385 | /**
386 | * Stops camera preview.
387 | *
388 | * The surface provided in the constructor already manages [startPreview] and [stopPreview].
389 | * Use this method only if you need to explicitly stop preview.
390 | *
391 | * @see [startPreview]
392 | */
393 | fun stopPreview() = apiVideoView.stopPreview()
394 |
395 | /**
396 | * Release internal elements.
397 | *
398 | * You won't be able to use this instance after calling this method.
399 | */
400 | fun release() {
401 | streamer.release()
402 | scope.cancel()
403 | }
404 | }
--------------------------------------------------------------------------------
/livestream/src/main/java/video/api/livestream/ConfigurationHelper.kt:
--------------------------------------------------------------------------------
1 | package video.api.livestream
2 |
3 | import android.content.Context
4 | import android.media.MediaFormat
5 | import io.github.thibaultbee.streampack.streamers.helpers.AudioStreamerConfigurationHelper
6 | import io.github.thibaultbee.streampack.streamers.helpers.CameraStreamerConfigurationHelper
7 | import io.github.thibaultbee.streampack.streamers.helpers.VideoCameraStreamerConfigurationHelper
8 | import io.github.thibaultbee.streampack.utils.backCameraList
9 | import io.github.thibaultbee.streampack.utils.cameraList
10 | import io.github.thibaultbee.streampack.utils.frontCameraList
11 |
12 | object ConfigurationHelper {
13 | private val helper = CameraStreamerConfigurationHelper.flvHelper
14 | val audio = AudioConfigurationHelper(helper.audio)
15 | val video = VideoStreamerConfigurationHelper(helper.video)
16 | }
17 |
18 | class AudioConfigurationHelper(private val audioHelper: AudioStreamerConfigurationHelper) {
19 | /**
20 | * Get supported bitrate range.
21 | *
22 | * @return bitrate range
23 | */
24 | fun getSupportedBitrates() =
25 | audioHelper.getSupportedBitrates(MediaFormat.MIMETYPE_AUDIO_AAC)
26 |
27 | /**
28 | * Get maximum supported number of channel by encoder.
29 | *
30 | * @return maximum number of channel supported by the encoder
31 | */
32 | fun getSupportedInputChannelRange() =
33 | audioHelper.getSupportedInputChannelRange(MediaFormat.MIMETYPE_AUDIO_AAC)
34 |
35 | /**
36 | * Get audio supported sample rates.
37 | *
38 | * @return sample rates list in Hz.
39 | */
40 | fun getSupportedSampleRates() =
41 | audioHelper.getSupportedSampleRates(MediaFormat.MIMETYPE_AUDIO_AAC)
42 | }
43 |
44 | class VideoStreamerConfigurationHelper(private val videoHelper: VideoCameraStreamerConfigurationHelper) {
45 |
46 | /**
47 | * Get supported bitrate range.
48 | *
49 | * @return bitrate range
50 | */
51 | fun getSupportedBitrates() =
52 | videoHelper.getSupportedBitrates(MediaFormat.MIMETYPE_VIDEO_AVC)
53 |
54 | /**
55 | * Get encoder supported resolutions range.
56 | *
57 | * @return pair that contains supported width ([Pair.first]) and supported height ([Pair.second]).
58 | */
59 | fun getSupportedResolutions() =
60 | videoHelper.getSupportedResolutions(MediaFormat.MIMETYPE_VIDEO_AVC)
61 |
62 | /**
63 | * Get camera supported resolutions that are also supported by the encoder.
64 | *
65 | * @param context application context
66 | * @return list of resolutions
67 | */
68 | fun getCameraSupportedResolutions(context: Context) =
69 | videoHelper.getSupportedResolutions(context, MediaFormat.MIMETYPE_VIDEO_AVC)
70 |
71 |
72 | /**
73 | * Get camera supported frame rate that are also supported by the encoder.
74 | *
75 | * @param context application context
76 | * @param cameraId camera id
77 | * @return list of frame rate
78 | */
79 | fun getSupportedFrameRates(
80 | context: Context,
81 | cameraId: String
82 | ) = videoHelper.getSupportedFramerates(context, MediaFormat.MIMETYPE_VIDEO_AVC, cameraId)
83 |
84 | /**
85 | * Get cameras list
86 | *
87 | * @param context application context
88 | * @return list of camera
89 | */
90 | fun getCamerasList(context: Context) = context.cameraList
91 |
92 | /**
93 | * Get back cameras list
94 | *
95 | * @param context application context
96 | * @return list of back camera
97 | */
98 | fun getBackCamerasList(context: Context) = context.backCameraList
99 |
100 | /**
101 | * Get front cameras list
102 | *
103 | * @param context application context
104 | * @return list of front camera
105 | */
106 | fun getFrontCamerasList(context: Context) = context.frontCameraList
107 | }
108 |
--------------------------------------------------------------------------------
/livestream/src/main/java/video/api/livestream/Extensions.kt:
--------------------------------------------------------------------------------
1 | package video.api.livestream
2 |
3 | import android.util.Size
4 | import kotlin.math.abs
5 |
6 | /**
7 | * Add a slash at the end of a [String] only if it is missing.
8 | *
9 | * @return the given string with a trailing slash.
10 | */
11 | fun String.addTrailingSlashIfNeeded(): String {
12 | return if (this.endsWith("/")) this else "$this/"
13 | }
14 |
15 | /**
16 | * Find the closest size to the given size in a list of sizes.
17 | */
18 | fun List.closestTo(size: Size): Size =
19 | this.minBy { abs((it.width * it.height) - (size.width * size.height)) }
--------------------------------------------------------------------------------
/livestream/src/main/java/video/api/livestream/enums/CameraFacingDirection.kt:
--------------------------------------------------------------------------------
1 | package video.api.livestream.enums
2 |
3 | import android.content.Context
4 | import io.github.thibaultbee.streampack.utils.backCameraList
5 | import io.github.thibaultbee.streampack.utils.frontCameraList
6 | import io.github.thibaultbee.streampack.utils.isBackCamera
7 | import io.github.thibaultbee.streampack.utils.isFrontCamera
8 |
9 | /**
10 | * Represents camera facing direction.
11 | */
12 | enum class CameraFacingDirection {
13 | /**
14 | * The facing of the camera is opposite to that of the screen.
15 | */
16 | BACK,
17 |
18 | /**
19 | * The facing of the camera is the same as that of the screen.
20 | */
21 | FRONT;
22 |
23 | /**
24 | * Returns the camera id from the camera facing direction.
25 | *
26 | * @param context the application context
27 | * @return the camera id
28 | */
29 | fun toCameraId(context: Context): String {
30 | val cameraList = if (this == BACK) {
31 | context.backCameraList
32 | } else {
33 | context.frontCameraList
34 | }
35 | return cameraList[0]
36 | }
37 |
38 | companion object {
39 | /**
40 | * Returns the camera facing direction from the camera id.
41 | *
42 | * @param context the application context
43 | * @param cameraId the camera id
44 | * @return the camera facing direction
45 | */
46 | fun fromCameraId(context: Context, cameraId: String): CameraFacingDirection {
47 | return when {
48 | context.isFrontCamera(cameraId) -> FRONT
49 | context.isBackCamera(cameraId) -> BACK
50 | else -> throw IllegalArgumentException("Unknown camera id: $cameraId")
51 | }
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/livestream/src/main/java/video/api/livestream/enums/Resolution.kt:
--------------------------------------------------------------------------------
1 | package video.api.livestream.enums
2 |
3 | import android.util.Size
4 |
5 | /**
6 | * Represents supported resolution.
7 | */
8 | enum class Resolution(val size: Size) {
9 | RESOLUTION_240(Size(352, 240)),
10 | RESOLUTION_360(Size(640, 360)),
11 | RESOLUTION_480(Size(854, 480)),
12 | RESOLUTION_720(Size(1280, 720)),
13 | RESOLUTION_1080(Size(1920, 1080));
14 |
15 | /**
16 | * Prints a [Resolution].
17 | *
18 | * @return a string containing "${width}x${heigth}"
19 | */
20 | override fun toString() = "${size.width}x${size.height}"
21 |
22 | companion object {
23 | /**
24 | * Converts from a [Size] to a [Resolution].
25 | *
26 | * @param size describing width and height dimensions in pixels
27 | * @return corresponding [Resolution]
28 | */
29 | fun valueOf(size: Size) =
30 | entries.first { it.size.width == size.width && it.size.height == size.height }
31 | }
32 | }
--------------------------------------------------------------------------------
/livestream/src/main/java/video/api/livestream/interfaces/IConnectionListener.kt:
--------------------------------------------------------------------------------
1 | package video.api.livestream.interfaces
2 |
3 | /**
4 | * Connection callbacks interface.
5 | * Use it to manage a connection.
6 | */
7 | interface IConnectionListener {
8 | /**
9 | * Triggered when connection failed.
10 | *
11 | * @param reason reason of connection failure
12 | */
13 | fun onConnectionFailed(reason: String)
14 |
15 | /**
16 | * Triggered when connection is successful.
17 | */
18 | fun onConnectionSuccess()
19 |
20 | /**
21 | * Triggered on disconnect event.
22 | */
23 | fun onDisconnect()
24 | }
--------------------------------------------------------------------------------
/livestream/src/main/java/video/api/livestream/models/AudioConfig.kt:
--------------------------------------------------------------------------------
1 | package video.api.livestream.models
2 |
3 | import android.media.AudioFormat
4 |
5 | /**
6 | * Describes audio configuration.
7 | */
8 | data class AudioConfig(
9 | /**
10 | * Audio bitrate in bps.
11 | */
12 | val bitrate: Int = 128000,
13 |
14 | /**
15 | * Audio sample rate in Hz.
16 | */
17 | val sampleRate: Int = 44100,
18 |
19 | /**
20 | * [Boolean.true] if you want audio capture in stereo,
21 | * [Boolean.false] for mono.
22 | */
23 | val stereo: Boolean = true,
24 |
25 | /**
26 | * [Boolean.true] if you want to activate echo canceler.
27 | * [Boolean.false] to deactivate.
28 | */
29 | val echoCanceler: Boolean = true,
30 |
31 | /**
32 | * [Boolean.true] if you want to activate noise suppressor.
33 | * [Boolean.false] to deactivate.
34 | */
35 | val noiseSuppressor: Boolean = true
36 | ) {
37 | internal fun toSdkConfig(): io.github.thibaultbee.streampack.data.AudioConfig {
38 | return io.github.thibaultbee.streampack.data.AudioConfig(
39 | startBitrate = bitrate,
40 | sampleRate = sampleRate,
41 | channelConfig = if (stereo) AudioFormat.CHANNEL_IN_STEREO else AudioFormat.CHANNEL_IN_MONO,
42 | enableEchoCanceler = echoCanceler,
43 | enableNoiseSuppressor = noiseSuppressor
44 | )
45 | }
46 |
47 | companion object {
48 | internal fun fromSdkConfig(config: io.github.thibaultbee.streampack.data.AudioConfig): AudioConfig {
49 | return AudioConfig(
50 | bitrate = config.startBitrate,
51 | sampleRate = config.sampleRate,
52 | stereo = config.channelConfig == AudioFormat.CHANNEL_IN_STEREO,
53 | echoCanceler = config.enableEchoCanceler,
54 | noiseSuppressor = config.enableNoiseSuppressor
55 | )
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/livestream/src/main/java/video/api/livestream/models/VideoConfig.kt:
--------------------------------------------------------------------------------
1 | package video.api.livestream.models
2 |
3 | import android.util.Size
4 | import video.api.livestream.enums.Resolution
5 |
6 | /**
7 | * Describes video configuration.
8 | */
9 | class VideoConfig(
10 | /**
11 | * Video resolution.
12 | * @see [Resolution]
13 | */
14 | val resolution: Size = Resolution.RESOLUTION_720.size,
15 |
16 | /**
17 | * Video bitrate in bps.
18 | */
19 | val bitrate: Int = getDefaultBitrate(resolution),
20 |
21 | /**
22 | * Video frame rate.
23 | */
24 | val fps: Int = 30,
25 |
26 | /**
27 | * The time interval between two consecutive key frames.
28 | */
29 | val gopDuration: Float = 1f,
30 | ) {
31 | constructor(
32 | resolution: Resolution,
33 | bitrate: Int,
34 | fps: Int,
35 | gopDuration: Float
36 | ) : this(resolution.size, bitrate, fps, gopDuration)
37 |
38 | internal fun toSdkConfig(): io.github.thibaultbee.streampack.data.VideoConfig {
39 | return io.github.thibaultbee.streampack.data.VideoConfig(
40 | startBitrate = bitrate,
41 | resolution = resolution,
42 | fps = fps,
43 | gopDuration = gopDuration
44 | )
45 | }
46 |
47 | companion object {
48 | internal fun fromSdkConfig(config: io.github.thibaultbee.streampack.data.VideoConfig): VideoConfig {
49 | return VideoConfig(
50 | bitrate = config.startBitrate,
51 | resolution = config.resolution,
52 | fps = config.fps,
53 | gopDuration = config.gopDuration
54 | )
55 | }
56 |
57 | private fun getDefaultBitrate(size: Size): Int {
58 | return when (size.width * size.height) {
59 | in 0..102_240 -> 800_000 // for 4/3 and 16/9 240p
60 | in 102_241..230_400 -> 1_000_000 // for 16/9 360p
61 | in 230_401..409_920 -> 1_300_000 // for 4/3 and 16/9 480p
62 | in 409_921..921_600 -> 2_000_000 // for 4/3 600p, 4/3 768p and 16/9 720p
63 | else -> 3_000_000 // for 16/9 1080p
64 | }
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/livestream/src/main/java/video/api/livestream/views/ApiVideoView.kt:
--------------------------------------------------------------------------------
1 | package video.api.livestream.views
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.util.Log
6 | import android.view.ViewGroup
7 | import android.widget.FrameLayout
8 | import io.github.thibaultbee.streampack.streamers.interfaces.ICameraStreamer
9 | import io.github.thibaultbee.streampack.views.PreviewView
10 |
11 | /**
12 | * View where to display camera preview.
13 | */
14 | class ApiVideoView @JvmOverloads constructor(
15 | context: Context,
16 | attrs: AttributeSet? = null,
17 | defStyle: Int = 0
18 | ) : FrameLayout(context, attrs, defStyle) {
19 | private val previewView = PreviewView(context, attrs, defStyle)
20 |
21 | internal var streamer: ICameraStreamer?
22 | get() = previewView.streamer
23 | /**
24 | * Set the [ICameraStreamer] to use.
25 | *
26 | * @param value the [ICameraStreamer] to use
27 | */
28 | set(value) {
29 | try {
30 | previewView.streamer = value
31 | } catch (t: Throwable) {
32 | Log.w(TAG, "Failed to set streamer: $t")
33 | }
34 | }
35 |
36 | init {
37 | addView(
38 | previewView, ViewGroup.LayoutParams(
39 | ViewGroup.LayoutParams.MATCH_PARENT,
40 | ViewGroup.LayoutParams.MATCH_PARENT
41 | )
42 | )
43 | }
44 |
45 | /**
46 | * Manually trigger measure & layout, as RN on Android skips those.
47 | * See comment on https://github.com/facebook/react-native/issues/17968#issuecomment-721958427
48 | */
49 | override fun requestLayout() {
50 | super.requestLayout()
51 | post(measureAndLayout)
52 | }
53 |
54 | private val measureAndLayout = Runnable {
55 | measure(
56 | MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
57 | MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
58 | )
59 | layout(left, top, right, bottom)
60 | }
61 |
62 | internal fun stopPreview() {
63 | previewView.stopPreview()
64 | }
65 |
66 | internal fun startPreview() {
67 | previewView.startPreview()
68 | }
69 |
70 | companion object {
71 | private const val TAG = "ApiVideoView"
72 | }
73 | }
--------------------------------------------------------------------------------
/livestream/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | rtmp://broadcast.api.video/s/
4 |
--------------------------------------------------------------------------------
/livestream/src/test/java/video/api/livestream/ExtensionsKtTest.kt:
--------------------------------------------------------------------------------
1 | package video.api.livestream
2 |
3 | import org.junit.Assert.*
4 | import org.junit.Test
5 |
6 | class ExtensionsKtTest {
7 |
8 | @Test
9 | fun addTrailingSlashIfNeeded() {
10 | assertEquals("abcde/", "abcde".addTrailingSlashIfNeeded())
11 | assertEquals("abcde/", "abcde/".addTrailingSlashIfNeeded())
12 | }
13 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
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 | }
14 | }
15 | rootProject.name = "ApiVideoLiveStream"
16 | include ':livestream'
17 | include ':example'
18 |
--------------------------------------------------------------------------------