├── .github └── workflows │ ├── build.yml │ ├── create-release-from-changelog.yml │ ├── release.yml │ ├── test.yml │ └── update-documentation.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.gradle ├── docs ├── docs_logo.svg └── logo-icon.svg ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lib ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── maven-push.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── video │ │ └── api │ │ └── rtmpdroid │ │ ├── Extensions.kt │ │ ├── RtmpMessageTest.kt │ │ ├── RtmpServer.kt │ │ ├── RtmpTest.kt │ │ └── amf │ │ └── AmfEncoderTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── cpp │ │ ├── CMakeLists.txt │ │ ├── Log.h │ │ ├── glue.cpp │ │ ├── models │ │ │ ├── RtmpContext.h │ │ │ ├── RtmpPacket.h │ │ │ └── RtmpWrapper.h │ │ └── patches │ │ │ ├── 0001-Port-to-openssl-1.1.1.patch │ │ │ ├── 0002-Add-CMakeLists.txt.patch │ │ │ ├── 0003-Fix-AMF_EncodeString-size-check.patch │ │ │ ├── 0004-Modernize-socket-API-usage.patch │ │ │ ├── 0005-Shutdown-socket-on-close-to-interrupt-socket-connect.patch │ │ │ ├── 0006-Add-support-for-enhanced-RTMP.patch │ │ │ └── 0007-When-packet-are-not-in-order-force-the-header-of-typ.patch │ └── java │ │ └── video │ │ └── api │ │ └── rtmpdroid │ │ ├── Rtmp.kt │ │ ├── RtmpNativeLoader.kt │ │ ├── RtmpPacket.kt │ │ ├── amf │ │ ├── AmfEncoder.kt │ │ └── models │ │ │ ├── EcmaArray.kt │ │ │ ├── NamedParameter.kt │ │ │ ├── NullParameter.kt │ │ │ └── ObjectParameter.kt │ │ └── internal │ │ └── VideoCodecs.kt │ └── test │ └── java │ └── video │ └── api │ └── rtmpdroid │ └── internal │ ├── ExVideoCodecsTest.kt │ └── VideoCodecsTest.kt └── settings.gradle /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Android build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-java@v2 11 | with: 12 | java-version: 17 13 | distribution: 'adopt' 14 | - name: Setup git config (for patch command) 15 | run: | 16 | git config --global user.name "GitHub Actions Bot" 17 | git config --global user.email "<>" 18 | - name: Grant execute permission for gradlew 19 | run: chmod +x gradlew 20 | - name: Build with Gradle 21 | run: ./gradlew build 22 | -------------------------------------------------------------------------------- /.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: Release Android package to Maven Central Repository 2 | on: 3 | release: 4 | types: [ published ] 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-java@v2 11 | with: 12 | java-version: 17 13 | distribution: 'adopt' 14 | - name: Setup git config (for patch command) 15 | run: | 16 | git config --global user.name "GitHub Actions Bot" 17 | git config --global user.email "<>" 18 | - name: Validate Gradle wrapper 19 | uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b 20 | - name: Decode the secret key 21 | run: echo $GPG_KEYRING_FILE_CONTENT | base64 --decode > ~/secring.gpg 22 | env: 23 | GPG_KEYRING_FILE_CONTENT: "${{ secrets.GPG_KEYRING_FILE_CONTENT }}" 24 | - name: Make gradlew executable 25 | run: chmod +x ./gradlew 26 | - name: Publish package 27 | run: ./gradlew publish -Psigning.secretKeyRingFile=$(echo ~/secring.gpg) -Psigning.password=$GPG_PASSPHRASE -Psigning.keyId=$GPG_KEY_ID 28 | env: 29 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} 30 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} 31 | GPG_KEY_ID: ${{ secrets.GPG_KEY_ID }} 32 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | unit-tests: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-java@v2 11 | with: 12 | java-version: 17 13 | distribution: 'adopt' 14 | - name: Make gradlew executable 15 | run: chmod +x ./gradlew 16 | - name: Run unit test 17 | run: ./gradlew test 18 | instrumented-tests: 19 | runs-on: macos-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-java@v2 23 | with: 24 | java-version: 17 25 | distribution: 'adopt' 26 | - name: Make gradlew executable 27 | run: chmod +x ./gradlew 28 | - name: Run Android Tests 29 | uses: reactivecircus/android-emulator-runner@v2 30 | with: 31 | api-level: 30 32 | arch: x86_64 33 | script: ./gradlew connectedCheck 34 | env: 35 | INTEGRATION_TESTS_API_TOKEN: ${{ secrets.INTEGRATION_TESTS_API_TOKEN }} 36 | -------------------------------------------------------------------------------- /.github/workflows/update-documentation.yml: -------------------------------------------------------------------------------- 1 | name: Update documentation 2 | on: 3 | release: 4 | types: [ published ] 5 | jobs: 6 | update-api-documentation: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-java@v2 11 | with: 12 | java-version: 17 13 | distribution: 'adopt' 14 | - name: Grant execute permission for gradlew 15 | run: chmod +x gradlew 16 | - name: Generate API documentation 17 | run: ./gradlew dokkaHtml 18 | - name: Deploy API documentation to Github Pages 19 | uses: JamesIves/github-pages-deploy-action@v4 20 | with: 21 | github_token: ${{ secrets.GITHUB_TOKEN }} 22 | branch: gh-pages 23 | folder: lib/build/dokka/html -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All changes to this project will be documented in this file. 4 | 5 | ## [1.2.1] - 2024-01-03 6 | 7 | - Force usage of message with header type 0 when packet timestamp are back in time 8 | 9 | ## [1.2.0] - 2023-12-21 10 | 11 | - Introducing packed version: all shared libraries are packed into `librtmpdroid.so` 12 | - Upgrade to AGP 8.0 13 | - Upgrade openssl to 3.0.12 14 | - Fix synchronization issue in `write(ByteArray)` 15 | 16 | ## [1.1.0] - 2023-10-23 17 | 18 | - Add API to support enhanced RTMP features 19 | - Upgrade dependencies (Kotlin,...) (incl. openssl to 3.0.9) 20 | 21 | ## [1.0.5] - 2022-11-07 22 | 23 | - Upgrade openssl to 3.0.7 24 | - Fix tcUrl length 25 | 26 | ## [1.0.4] - 2022-10-06 27 | 28 | - Fix a crash when freeing url in `nativeClose`. 29 | See [#14](https://github.com/apivideo/api.video-flutter-live-stream/issues/14) 30 | and [#33](https://github.com/apivideo/api.video-reactnative-live-stream/issues/33). 31 | 32 | ## [1.0.3] - 2022-08-05 33 | 34 | - Shutdown socket to interrupt socket long connection 35 | 36 | ## [1.0.2] - 2022-05-30 37 | 38 | - Do not obfuscate `video.api.rtmpdroid` classes 39 | 40 | ## [1.0.1] - 2022-03-30 41 | 42 | - Change `connect(url)` exception when URL is not valid to `IllegalArgumentException`. 43 | 44 | ## [1.0.0] - 2022-03-22 45 | 46 | - Initial version 47 | -------------------------------------------------------------------------------- /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) 2022 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 | [![badge](https://img.shields.io/twitter/follow/api_video?style=social)](https://twitter.com/intent/follow?screen_name=api_video) 2 |   [![badge](https://img.shields.io/github/stars/apivideo/api.video-rtmpdroid?style=social)](https://github.com/apivideo/api.video-rtmpdroid) 3 |   [![badge](https://img.shields.io/discourse/topics?server=https%3A%2F%2Fcommunity.api.video)](https://community.api.video) 4 | ![](https://github.com/apivideo/.github/blob/main/assets/apivideo_banner.png) 5 |

api.video Android live stream library

6 | 7 | [api.video](https://api.video) is the video infrastructure for product builders. Lightning fast 8 | video APIs for integrating, scaling, and managing on-demand & low latency live streaming features in 9 | your app. 10 | 11 | # Table of contents 12 | 13 | - [Table of contents](#table-of-contents) 14 | - [Project description](#project-description) 15 | - [Getting started](#getting-started) 16 | - [Installation](#installation) 17 | - [Gradle](#gradle) 18 | - [Permissions](#permissions) 19 | - [Documentation](#documentation) 20 | - [FAQ](#faq) 21 | 22 | # Project description 23 | 24 | When it comes to Real-Time Messaging Protocol (RTMP), the most popular implementation is the C 25 | library: [librtmp](http://git.ffmpeg.org/rtmpdump). However, if you are streaming from Android and 26 | want to use librtmp, you need to go through the following painful steps: 27 | 28 | - compile librtmp for all Android architectures to generate multiple shared libraries 29 | - write a code that links the librtmp C functions to the Java classes/methods using Java Native 30 | Interface (JNI) 31 | - write Java classes/methods 32 | 33 | rtmpdroid has been built to address this pain: you can directly use librtmp in your Android 34 | application like any other Java/Kotlin library. 35 | 36 | rtmpdroid also comes with a minimalist Action Message Format (AMF) encoder that is included in 37 | librtmp. 38 | 39 | # Getting started 40 | 41 | ## Installation 42 | 43 | ### Gradle 44 | 45 | In build.gradle, add the following code: 46 | 47 | ```groovy 48 | dependencies { 49 | implementation 'video.api:rtmpdroid:1.2.1' 50 | } 51 | ``` 52 | 53 | ## Code sample 54 | 55 | ### RTMP 56 | 57 | ```kotlin 58 | val rtmp = Rtmp() 59 | rtmp.connect("rtmp://broadcast.api.video/s/YOUR_STREAM_KEY") 60 | rtmp.connectStream() 61 | 62 | while (true) { 63 | val flvBuffer = getNewFlvFrame() // flvBuffer is a direct ByteBuffer 64 | rtmp.write(flvBuffer) 65 | } 66 | ``` 67 | 68 | ### AMF 69 | 70 | ```kotlin 71 | val amfEncoder = AmfEncoder() 72 | 73 | amfEncoder.add("myParam", 3.0) 74 | val ecmaArray = EcmaArray() 75 | ecmaArray.add("myOtherParam", "value") 76 | amfEncoder.add(ecmaArray) 77 | 78 | val amfBuffer = amfEncoder.encode() 79 | ``` 80 | 81 | ## Permissions 82 | 83 | ```xml 84 | 85 | 86 | 87 | 88 | ``` 89 | 90 | ## Packed version 91 | 92 | The default version of `rtmpdroid` comes with the following shared libraries: 93 | 94 | - librtmp.so 95 | - libssl.so 96 | - libcrypto.so 97 | - librtmpdroid.so 98 | 99 | However, your application might already use `libssl` and `libcrypto`. In this case, you can use the 100 | packed version of `rtmpdroid`. It only contains only `librtmpdroid.so` and the other libraries are 101 | contains in this library. 102 | To use the packed version, add a `-packed` suffix to the `rtmpdroid` version in your `build.gradle`: 103 | 104 | ```groovy 105 | dependencies { 106 | implementation 'video.api:rtmpdroid:1.2.0-packed' 107 | } 108 | ``` 109 | 110 | # Documentation 111 | 112 | * [API documentation](https://apivideo.github.io/api.video-rtmpdroid/) 113 | * [api.video documentation](https://docs.api.video) 114 | 115 | # FAQ 116 | 117 | If you have any questions, ask us in [https://community.api.video](https://community.api.video) or 118 | use [Issues]. 119 | 120 | 121 | [//]: # (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) 122 | 123 | [Issues]: 124 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext { 4 | kotlinVersion = '1.8.22' 5 | dokkaVersion = '1.8.20' 6 | } 7 | 8 | dependencies { 9 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" 10 | classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokkaVersion" 11 | 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | plugins { 18 | id 'com.android.application' version '8.2.0' apply false 19 | id 'com.android.library' version '8.2.0' apply false 20 | id 'org.jetbrains.kotlin.android' version "${kotlinVersion}" apply false 21 | } 22 | 23 | tasks.register('clean', Delete) { 24 | delete rootProject.buildDir 25 | } -------------------------------------------------------------------------------- /docs/docs_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Horizontal lockup 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/logo-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | LogoFinal Copy 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | 25 | POM_NAME=rtmpdroid 26 | POM_ARTIFACT_ID=rtmpdroid 27 | POM_PACKAGING=aar 28 | 29 | VERSION_NAME=1.2.1 30 | VERSION_CODE=001002001 31 | GROUP=video.api 32 | 33 | POM_DESCRIPTION=Android librtmp wrapper. 34 | POM_URL=https://github.com/apivideo/api.video-rtmpdroid 35 | POM_SCM_URL=https://github.com/apivideo/api.video-rtmpdroid.git 36 | POM_SCM_CONNECTION=scm:git@github.com:apivideo/api.video-rtmpdroid.git 37 | POM_SCM_DEV_CONNECTION=scm:git@github.com:apivideo/api.video-rtmpdroid.git 38 | POM_LICENCE_NAME=MIT Licence 39 | POM_LICENCE_URL=https://opensource.org/licenses/mit-license.php 40 | POM_LICENCE_DIST=repo 41 | POM_DEVELOPER_ID=api.video 42 | POM_DEVELOPER_NAME=Ecosystem Team -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apivideo/api.video-rtmpdroid/d7eb771f960ab4131409b0cd1bcdb93741ef6f85/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Dec 19 18:05:56 CET 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /lib/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /lib/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'org.jetbrains.dokka' 5 | } 6 | apply from: 'maven-push.gradle' 7 | 8 | android { 9 | namespace 'video.api.rtmpdroid' 10 | 11 | ndkVersion "26.1.10909125" 12 | 13 | defaultConfig { 14 | minSdk 21 15 | compileSdk 34 16 | targetSdk 34 17 | 18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 19 | consumerProguardFiles "consumer-rules.pro" 20 | } 21 | 22 | externalNativeBuild { 23 | cmake { 24 | path "src/main/cpp/CMakeLists.txt" 25 | } 26 | } 27 | 28 | buildTypes { 29 | release { 30 | minifyEnabled false 31 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 32 | } 33 | } 34 | 35 | flavorDimensions "packaging" 36 | productFlavors { 37 | packed { 38 | dimension "packaging" 39 | versionNameSuffix "-packed" 40 | externalNativeBuild.cmake { 41 | arguments '-DPACKAGING=PACKED' 42 | } 43 | versionName "$VERSION_NAME-packed" 44 | } 45 | unpacked { 46 | dimension "packaging" 47 | externalNativeBuild.cmake { 48 | arguments '-DPACKAGING=UNPACKED' 49 | } 50 | versionName VERSION_NAME 51 | } 52 | } 53 | 54 | compileOptions { 55 | sourceCompatibility JavaVersion.VERSION_1_8 56 | targetCompatibility JavaVersion.VERSION_1_8 57 | } 58 | kotlinOptions { 59 | jvmTarget = '1.8' 60 | } 61 | 62 | publishing { 63 | multipleVariants { 64 | allVariants() 65 | withJavadocJar() 66 | withSourcesJar() 67 | } 68 | } 69 | 70 | } 71 | 72 | dokkaHtml { 73 | moduleName.set("api.video ${rootProject.name} library") 74 | suppressInheritedMembers.set(true) 75 | 76 | dokkaSourceSets { 77 | named("main") { 78 | noAndroidSdkLink.set(false) 79 | skipDeprecated.set(true) 80 | includeNonPublic.set(false) 81 | skipEmptyPackages.set(true) 82 | } 83 | } 84 | pluginsMapConfiguration.set( 85 | ["org.jetbrains.dokka.base.DokkaBase": """{ 86 | "customAssets" : [ 87 | "${file("$rootDir/docs/docs_logo.svg")}", 88 | "${file("$rootDir/docs/logo-icon.svg")}" 89 | ] 90 | }"""] 91 | ) 92 | } 93 | 94 | dependencies { 95 | 96 | implementation 'androidx.core:core-ktx:1.12.0' 97 | implementation 'androidx.appcompat:appcompat:1.6.1' 98 | testImplementation 'junit:junit:4.13.2' 99 | androidTestImplementation 'androidx.test.ext:junit:1.1.5' 100 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 101 | } -------------------------------------------------------------------------------- /lib/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | # Keep rtmpdroid classes 2 | -keep class video.api.rtmpdroid.** { *; } 3 | -dontwarn video.api.rtmpdroid.** -------------------------------------------------------------------------------- /lib/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('OSSRH_USERNAME') ? OSSRH_USERNAME : System.getenv("OSSRH_USERNAME") 36 | } 37 | 38 | def getRepositoryPassword() { 39 | return hasProperty('OSSRH_PASSWORD') ? OSSRH_PASSWORD : System.getenv("OSSRH_PASSWORD") 40 | } 41 | 42 | afterEvaluate { project -> 43 | publishing { 44 | publications { 45 | android.libraryVariants.all { variant -> 46 | if (variant.buildType.name == "release") { 47 | "${variant.name}"(MavenPublication) { 48 | from components.findByName("${variant.name}") 49 | groupId GROUP 50 | artifactId POM_ARTIFACT_ID 51 | version "${variant.getMergedFlavor().versionName}" 52 | 53 | pom { 54 | name = POM_NAME 55 | packaging = POM_PACKAGING 56 | description = POM_DESCRIPTION 57 | url = POM_URL 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 | } 83 | } 84 | repositories { 85 | maven { 86 | url = isReleaseBuild() ? getReleaseRepositoryUrl() : getSnapshotRepositoryUrl() 87 | 88 | credentials { 89 | username = getRepositoryUsername() 90 | password = getRepositoryPassword() 91 | } 92 | } 93 | } 94 | } 95 | 96 | signing { 97 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } 98 | sign publishing.publications 99 | } 100 | } -------------------------------------------------------------------------------- /lib/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 -------------------------------------------------------------------------------- /lib/src/androidTest/java/video/api/rtmpdroid/Extensions.kt: -------------------------------------------------------------------------------- 1 | package video.api.rtmpdroid 2 | 3 | import java.nio.ByteBuffer 4 | 5 | fun Double.toByteArray(): ByteArray = ByteBuffer.allocate(8).putDouble(this).array() 6 | 7 | /** 8 | * Returns [ByteBuffer] array even if [ByteBuffer.hasArray] returns false. 9 | * 10 | * @return [ByteArray] extracted from [ByteBuffer] 11 | */ 12 | fun ByteBuffer.extractArray(): ByteArray { 13 | return if (this.hasArray()) { 14 | this.array() 15 | } else { 16 | val byteArray = ByteArray(this.remaining()) 17 | this.get(byteArray) 18 | byteArray 19 | } 20 | } -------------------------------------------------------------------------------- /lib/src/androidTest/java/video/api/rtmpdroid/RtmpMessageTest.kt: -------------------------------------------------------------------------------- 1 | package video.api.rtmpdroid 2 | 3 | import org.junit.After 4 | import org.junit.Assert.assertArrayEquals 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import video.api.rtmpdroid.amf.AmfEncoder 8 | import java.nio.ByteBuffer 9 | 10 | /** 11 | * Check that methods correctly answer. 12 | */ 13 | class RtmpMessageTest { 14 | private val rtmp = Rtmp() 15 | private val rtmpServer = RtmpServer() 16 | 17 | companion object { 18 | private const val FLV_HEADER_TAG_SIZE = 11 19 | } 20 | 21 | private fun writeScriptData(payload: ByteBuffer): ByteBuffer { 22 | val dataSize = payload.remaining().toShort() 23 | val flvTagSize = FLV_HEADER_TAG_SIZE + dataSize 24 | val buffer = 25 | ByteBuffer.allocateDirect(flvTagSize + 4) // 4 - PreviousTagSize 26 | 27 | // FLV Tag 28 | buffer.put(18) // script data 29 | buffer.put(0) 30 | buffer.putShort(dataSize) 31 | buffer.putInt(0) // ts 32 | buffer.put(0) // 24 bit for Stream ID 33 | buffer.putShort(0) // 24 bit for Stream ID 34 | 35 | buffer.put(payload) 36 | 37 | buffer.putInt(flvTagSize) 38 | 39 | buffer.rewind() 40 | 41 | return buffer 42 | } 43 | 44 | private fun createFakeFlvBuffer(): ByteBuffer { 45 | val amfEncoder = AmfEncoder() 46 | amfEncoder.add("myField", 4.0) 47 | return writeScriptData(amfEncoder.encode()) 48 | } 49 | 50 | private fun createFakeFlvArray(): ByteArray { 51 | val buffer = createFakeFlvBuffer() 52 | return buffer.array().sliceArray(IntRange(4, 4 + buffer.limit() - 1)) 53 | } 54 | 55 | @After 56 | fun tearDown() { 57 | rtmp.close() 58 | rtmpServer.shutdown() 59 | } 60 | 61 | @Test 62 | fun connectTest() { 63 | val futureData = rtmpServer.enqueueConnect() 64 | rtmp.connect("rtmp://127.0.0.1:${rtmpServer.port}/app/playpath") 65 | rtmp.connectStream() 66 | assertEquals(true, futureData.get()) 67 | } 68 | 69 | @Test 70 | fun writeByteArrayTest() { 71 | val expectedArray = createFakeFlvArray() 72 | val futureData = rtmpServer.enqueueRead() 73 | rtmp.connect("rtmp://127.0.0.1:${rtmpServer.port}/app/playpath") 74 | rtmp.connectStream() 75 | rtmp.write(expectedArray) 76 | val resultBuffer = futureData.get() 77 | assertArrayEquals( 78 | expectedArray.sliceArray(IntRange(11, expectedArray.size - 5)), 79 | resultBuffer.extractArray().sliceArray(IntRange(16, resultBuffer.limit() - 1)) 80 | ) 81 | } 82 | 83 | @Test 84 | fun writeByteBufferTest() { 85 | val expectedBuffer = createFakeFlvBuffer() 86 | expectedBuffer.rewind() 87 | val futureData = rtmpServer.enqueueRead() 88 | rtmp.connect("rtmp://127.0.0.1:${rtmpServer.port}/app/playpath") 89 | rtmp.connectStream() 90 | rtmp.write(expectedBuffer) 91 | val resultBuffer = futureData.get() 92 | assertArrayEquals( 93 | expectedBuffer.extractArray().sliceArray(IntRange(15, expectedBuffer.limit() - 1)), 94 | resultBuffer.extractArray().sliceArray(IntRange(16, resultBuffer.limit() - 1)) 95 | ) 96 | } 97 | } -------------------------------------------------------------------------------- /lib/src/androidTest/java/video/api/rtmpdroid/RtmpServer.kt: -------------------------------------------------------------------------------- 1 | package video.api.rtmpdroid 2 | 3 | import android.os.ParcelFileDescriptor 4 | import video.api.rtmpdroid.amf.AmfEncoder 5 | import video.api.rtmpdroid.amf.models.NullParameter 6 | import video.api.rtmpdroid.amf.models.ObjectParameter 7 | import java.net.ServerSocket 8 | import java.nio.ByteBuffer 9 | import java.util.concurrent.Callable 10 | import java.util.concurrent.Executors 11 | import java.util.concurrent.Future 12 | 13 | class RtmpServer { 14 | private val executor = Executors.newCachedThreadPool() 15 | private val serverSocket = ServerSocket(0) 16 | 17 | val port: Int = serverSocket.localPort 18 | 19 | private fun sendConnectResult(rtmp: Rtmp, transactionId: Int) { 20 | val amfEncoder = AmfEncoder().apply { 21 | add("_result") 22 | add(transactionId.toDouble()) 23 | // Information 24 | val objectParameter = ObjectParameter() 25 | objectParameter.add("level", "status") 26 | objectParameter.add("code", "NetConnection.Connect.Success") 27 | objectParameter.add("description", "Connection succeeded.") 28 | add(objectParameter) 29 | } 30 | val body = amfEncoder.encode() 31 | val packet = RtmpPacket(0x03, 1, PacketType.COMMAND, 0, body) 32 | rtmp.writePacket(packet) 33 | } 34 | 35 | private fun sendResultNumber(rtmp: Rtmp, transactionId: Int, streamId: Int) { 36 | val amfEncoder = AmfEncoder().apply { 37 | add("_result") 38 | add(transactionId.toDouble()) 39 | add(NullParameter()) 40 | add(streamId.toDouble()) 41 | } 42 | val body = amfEncoder.encode() 43 | val packet = RtmpPacket(0x03, 1, PacketType.COMMAND, 0, body) 44 | rtmp.writePacket(packet) 45 | } 46 | 47 | private fun sendOnStatus(rtmp: Rtmp, transactionId: Int) { 48 | val amfEncoder = AmfEncoder().apply { 49 | add("onStatus") 50 | add(0.0) 51 | add(NullParameter()) 52 | // Information 53 | val objectParameter = ObjectParameter() 54 | objectParameter.add("level", "status") 55 | objectParameter.add("code", "NetStream.Publish.Start") 56 | objectParameter.add("description", "Publish started.") 57 | add(objectParameter) 58 | } 59 | val body = amfEncoder.encode() 60 | val packet = RtmpPacket(0x03, 1, PacketType.COMMAND, 0, body) 61 | rtmp.writePacket(packet) 62 | } 63 | 64 | private fun invokeServer(rtmp: Rtmp, fd: Int) { 65 | rtmp.serve(fd) 66 | var packet = rtmp.readPacket() // connect 67 | sendConnectResult(rtmp, 1) 68 | packet = rtmp.readPacket() // releaseStream 69 | packet = rtmp.readPacket() // FCPublish 70 | packet = rtmp.readPacket() // createStream 71 | sendResultNumber(rtmp, 4, 1) // createStream - result 72 | packet = rtmp.readPacket() // publish 73 | sendOnStatus(rtmp, 5) 74 | } 75 | 76 | fun enqueueConnect(): Future { 77 | return executor.submit(Callable { 78 | val clientSocket = serverSocket.accept() 79 | Rtmp().use { 80 | invokeServer(it, ParcelFileDescriptor.fromSocket(clientSocket).detachFd()) 81 | } 82 | true 83 | }) 84 | } 85 | 86 | fun enqueueRead(): Future { 87 | return executor.submit(Callable { 88 | val clientSocket = serverSocket.accept() 89 | Rtmp().use { 90 | invokeServer(it, ParcelFileDescriptor.fromSocket(clientSocket).detachFd()) 91 | val packet = it.readPacket() 92 | packet.buffer 93 | } 94 | }) 95 | } 96 | 97 | fun shutdown() { 98 | serverSocket.close() 99 | executor.shutdown() 100 | } 101 | } -------------------------------------------------------------------------------- /lib/src/androidTest/java/video/api/rtmpdroid/RtmpTest.kt: -------------------------------------------------------------------------------- 1 | package video.api.rtmpdroid 2 | 3 | import android.media.MediaFormat 4 | import org.junit.After 5 | import org.junit.Assert.* 6 | import org.junit.Test 7 | import java.net.SocketException 8 | import java.net.SocketTimeoutException 9 | import java.nio.ByteBuffer 10 | 11 | /** 12 | * Check that methods correctly answer. 13 | */ 14 | class RtmpTest { 15 | private val rtmp = Rtmp(enableWrite = true) 16 | 17 | @After 18 | fun tearDown() { 19 | rtmp.close() 20 | } 21 | 22 | @Test 23 | fun isConnectedTest() { 24 | assertFalse(rtmp.isConnected) 25 | } 26 | 27 | @Test 28 | fun timeoutTest() { 29 | val timeout = 1234 30 | rtmp.timeout = timeout 31 | assertEquals(timeout, rtmp.timeout) 32 | } 33 | 34 | @Test 35 | fun supportedVideoCodecsTest() { 36 | rtmp.supportedVideoCodecs = listOf(MediaFormat.MIMETYPE_VIDEO_AVC) 37 | assertTrue(rtmp.supportedVideoCodecs.size == 1) 38 | assertTrue(rtmp.supportedVideoCodecs.contains(MediaFormat.MIMETYPE_VIDEO_AVC)) 39 | 40 | rtmp.supportedVideoCodecs = 41 | listOf(MediaFormat.MIMETYPE_VIDEO_AVC, MediaFormat.MIMETYPE_VIDEO_HEVC, MediaFormat.MIMETYPE_VIDEO_AV1) 42 | assertTrue(rtmp.supportedVideoCodecs.size == 3) 43 | assertTrue(rtmp.supportedVideoCodecs.contains(MediaFormat.MIMETYPE_VIDEO_AVC)) 44 | assertTrue(rtmp.supportedVideoCodecs.contains(MediaFormat.MIMETYPE_VIDEO_HEVC)) 45 | } 46 | 47 | @Test 48 | fun connectTest() { 49 | try { 50 | rtmp.connect("rtmp://0.0.0.0:1935/hhs/abcde live=1") 51 | } catch (_: SocketException) { 52 | } 53 | } 54 | 55 | @Test 56 | fun connectStreamTest() { 57 | try { 58 | rtmp.connectStream() 59 | } catch (_: SocketException) { 60 | } 61 | } 62 | 63 | @Test 64 | fun deleteStreamTest() { 65 | try { 66 | rtmp.deleteStream() 67 | } catch (_: SocketException) { 68 | } 69 | } 70 | 71 | @Test 72 | fun readTest() { 73 | val data = ByteArray(10) 74 | try { 75 | rtmp.read(data) 76 | } catch (_: SocketTimeoutException) { 77 | } 78 | } 79 | 80 | @Test 81 | fun writeByteBufferTest() { 82 | val buffer = ByteBuffer.allocateDirect(10) 83 | try { 84 | rtmp.write(buffer) 85 | } catch (_: SocketTimeoutException) { 86 | } 87 | } 88 | 89 | @Test 90 | fun writeByteArrayTest() { 91 | val data = byteArrayOf(10, 20) 92 | try { 93 | rtmp.write(data) 94 | } catch (_: SocketTimeoutException) { 95 | } 96 | } 97 | 98 | @Test 99 | fun pauseTest() { 100 | try { 101 | rtmp.pause() 102 | } catch (_: SocketException) { 103 | } 104 | } 105 | 106 | @Test 107 | fun resumeTest() { 108 | try { 109 | rtmp.resume() 110 | } catch (_: SocketException) { 111 | } 112 | } 113 | 114 | @Test 115 | fun closeTest() { 116 | try { 117 | rtmp.close() 118 | } catch (_: SocketException) { 119 | fail("close must not throw an exception") 120 | } 121 | } 122 | 123 | @Test 124 | fun deleteStreamAfterClose() { 125 | try { 126 | rtmp.close() 127 | rtmp.deleteStream() 128 | fail("deleteStream must throw an exception if close has been called") 129 | } catch (_: Exception) { 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /lib/src/androidTest/java/video/api/rtmpdroid/amf/AmfEncoderTest.kt: -------------------------------------------------------------------------------- 1 | package video.api.rtmpdroid.amf 2 | 3 | import org.junit.Assert.assertArrayEquals 4 | import org.junit.Assert.assertEquals 5 | import org.junit.Test 6 | import video.api.rtmpdroid.amf.models.EcmaArray 7 | import video.api.rtmpdroid.amf.models.NamedParameter 8 | import video.api.rtmpdroid.toByteArray 9 | 10 | /** 11 | * Check that methods correctly answer. 12 | */ 13 | class AmfEncoderTest { 14 | private val amfEncoder = AmfEncoder() 15 | 16 | @Test 17 | fun getBufferSizeTest() { 18 | amfEncoder.add(4) 19 | assertEquals(4, amfEncoder.minBufferSize) 20 | 21 | val s = "Test" 22 | amfEncoder.add(s) 23 | assertEquals(4 + 3 + s.length, amfEncoder.minBufferSize) 24 | } 25 | 26 | @Test 27 | fun encodeBooleanTest() { 28 | val b = true 29 | amfEncoder.add(b) 30 | val buffer = amfEncoder.encode() 31 | 32 | val expectedArray = byteArrayOf( 33 | AmfType.BOOLEAN.value, if (b) { 34 | 1 35 | } else { 36 | 0 37 | } 38 | ) 39 | assertArrayEquals(expectedArray, buffer.array().sliceArray(IntRange(4, 4 + buffer.limit() - 1))) 40 | } 41 | 42 | @Test 43 | fun encodeIntTest() { 44 | val i = 4 45 | amfEncoder.add(i) 46 | val buffer = amfEncoder.encode() 47 | 48 | assertEquals(i, buffer.getInt(0)) 49 | } 50 | 51 | @Test 52 | fun encodeStringTest() { 53 | val s = "stringToEncode" 54 | amfEncoder.add(s) 55 | val buffer = amfEncoder.encode() 56 | 57 | val expectedArray = byteArrayOf(AmfType.STRING.value, 0, s.length.toByte()) + s.toByteArray() 58 | assertArrayEquals( 59 | expectedArray, 60 | buffer.array().sliceArray( 61 | IntRange( 62 | 4, 63 | 4 + buffer.limit() - 1 64 | ) 65 | ) // 4 bytes for direct byte buffer 66 | ) 67 | } 68 | 69 | @Test 70 | fun encodeNamedBooleanTest() { 71 | val p = NamedParameter("myBoolean", true) 72 | amfEncoder.add(p) 73 | val buffer = amfEncoder.encode() 74 | 75 | val expectedArray = byteArrayOf( 76 | 0x0, 77 | p.name.length.toByte() 78 | ) + p.name.toByteArray() + byteArrayOf( 79 | AmfType.BOOLEAN.value, if (p.value as Boolean) { 80 | 1 81 | } else { 82 | 0 83 | } 84 | ) 85 | assertArrayEquals( 86 | expectedArray, 87 | buffer.array().sliceArray(IntRange(4, 4 + buffer.limit() - 1)) 88 | ) // 4 bytes for direct byte buffer 89 | } 90 | 91 | @Test 92 | fun encodeNamedDoubleTest() { 93 | val p = NamedParameter("myNumber", 4.0) 94 | amfEncoder.add(p) 95 | val buffer = amfEncoder.encode() 96 | 97 | val expectedArray = byteArrayOf( 98 | 0x0, 99 | p.name.length.toByte() 100 | ) + p.name.toByteArray() + byteArrayOf(0) + (p.value as Double).toByteArray() 101 | assertArrayEquals( 102 | expectedArray, 103 | buffer.array().sliceArray(IntRange(4, 4 + buffer.limit() - 1)) 104 | ) // 4 bytes for direct byte buffer 105 | } 106 | 107 | @Test 108 | fun encodeNamedStringTest() { 109 | val p = NamedParameter("myString", "stringToEncode") 110 | amfEncoder.add(p) 111 | val buffer = amfEncoder.encode() 112 | 113 | val expectedArray = 114 | byteArrayOf(0x0, p.name.length.toByte()) + p.name.toByteArray() + byteArrayOf( 115 | AmfType.STRING.value, 116 | 0, 117 | (p.value as String).length.toByte() 118 | ) + (p.value as String).toByteArray() 119 | assertArrayEquals( 120 | expectedArray, 121 | buffer.array().sliceArray(IntRange(4, 4 + buffer.limit() - 1)) 122 | ) // 4 bytes for direct byte buffer 123 | } 124 | 125 | @Test 126 | fun encodeArrayWithIntTest() { 127 | val i = 4 128 | val a = EcmaArray() 129 | a.add(i) 130 | amfEncoder.add(a) 131 | val buffer = amfEncoder.encode() 132 | 133 | val expectedArray = byteArrayOf( 134 | AmfType.ECMA_ARRAY.value, 0, 0, 0, 1, // Array header 135 | 0, 0, 0, 4, // value 136 | 0, 0, AmfType.OBJECT_END.value // Array footer 137 | ) 138 | assertArrayEquals( 139 | expectedArray, 140 | buffer.array().sliceArray(IntRange(4, 4 + buffer.limit() - 1)) 141 | ) // 4 bytes for direct byte buffer 142 | } 143 | } -------------------------------------------------------------------------------- /lib/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /lib/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | project(rtmpdroid) 3 | 4 | include(ExternalProject) 5 | find_program(GIT "git") 6 | 7 | set(OPENSSL_VERSION "openssl-3.0.12") 8 | set(RTMP_VERSION "f1b83c10d8beb43fcc70a6e88cf4325499f25857") 9 | 10 | set(PACKAGING UNPACKED CACHE STRING "Set packaging type") 11 | set_property(CACHE PACKAGING PROPERTY STRINGS PACKED UNPACKED) 12 | 13 | if (${PACKAGING} STREQUAL "PACKED") 14 | set(ENABLE_SHARED OFF) 15 | set(OPENSSL_FEATURES no-shared) 16 | set(LIBRARY_FORMAT STATIC) 17 | set(LIBRARY_EXTENSION a) 18 | set(TARGET_LINK_LIBRARY rtmp crypto ssl z) 19 | else () 20 | set(ENABLE_SHARED ON) 21 | set(LIBRARY_FORMAT SHARED) 22 | set(LIBRARY_EXTENSION so) 23 | endif () 24 | 25 | # OpenSSL - needs few executable such as perl and mv in PATH 26 | ExternalProject_Add(openssl_project 27 | GIT_REPOSITORY https://github.com/openssl/openssl.git 28 | GIT_TAG ${OPENSSL_VERSION} 29 | CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env PATH=${ANDROID_TOOLCHAIN_ROOT}/bin:$ENV{PATH} CC=${CMAKE_C_COMPILER} ANDROID_NDK_ROOT=${ANDROID_NDK} perl /Configure android-${ANDROID_ARCH_NAME} --openssldir=${CMAKE_LIBRARY_OUTPUT_DIRECTORY} --libdir="" --prefix=${CMAKE_LIBRARY_OUTPUT_DIRECTORY} no-tests ${OPENSSL_FEATURES} -D__ANDROID_API__=${ANDROID_PLATFORM_LEVEL} 30 | BUILD_COMMAND ${CMAKE_COMMAND} -E env PATH=${ANDROID_TOOLCHAIN_ROOT}/bin:$ENV{PATH} ANDROID_NDK_ROOT=${ANDROID_NDK} make 31 | BUILD_BYPRODUCTS ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libssl.${LIBRARY_EXTENSION} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libcrypto.${LIBRARY_EXTENSION} 32 | BUILD_IN_SOURCE 1 33 | ) 34 | 35 | add_library(ssl ${LIBRARY_FORMAT} IMPORTED) 36 | add_dependencies(ssl openssl_project) 37 | set_target_properties(ssl PROPERTIES IMPORTED_LOCATION ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libssl.${LIBRARY_EXTENSION}) 38 | 39 | add_library(crypto ${LIBRARY_FORMAT} IMPORTED) 40 | add_dependencies(crypto openssl_project) 41 | set_target_properties(crypto PROPERTIES IMPORTED_LOCATION ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libcrypto.${LIBRARY_EXTENSION}) 42 | 43 | # RTMP 44 | ExternalProject_Add(rtmp_project 45 | GIT_REPOSITORY http://git.ffmpeg.org/rtmpdump 46 | GIT_TAG ${RTMP_VERSION} 47 | PATCH_COMMAND ${GIT} am ${CMAKE_CURRENT_SOURCE_DIR}/patches/0001-Port-to-openssl-1.1.1.patch 48 | && ${GIT} am ${CMAKE_CURRENT_SOURCE_DIR}/patches/0002-Add-CMakeLists.txt.patch 49 | && ${GIT} am ${CMAKE_CURRENT_SOURCE_DIR}/patches/0003-Fix-AMF_EncodeString-size-check.patch 50 | && ${GIT} am ${CMAKE_CURRENT_SOURCE_DIR}/patches/0004-Modernize-socket-API-usage.patch 51 | && ${GIT} am ${CMAKE_CURRENT_SOURCE_DIR}/patches/0005-Shutdown-socket-on-close-to-interrupt-socket-connect.patch 52 | && ${GIT} am ${CMAKE_CURRENT_SOURCE_DIR}/patches/0006-Add-support-for-enhanced-RTMP.patch 53 | && ${GIT} am ${CMAKE_CURRENT_SOURCE_DIR}/patches/0007-When-packet-are-not-in-order-force-the-header-of-typ.patch 54 | CMAKE_ARGS 55 | -DENABLE_EXAMPLES=OFF 56 | -DOPENSSL_INCLUDE_DIR=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/include 57 | -DOPENSSL_CRYPTO_LIBRARY=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libcrypto.${LIBRARY_EXTENSION} 58 | -DOPENSSL_SSL_LIBRARY=${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/libssl.${LIBRARY_EXTENSION} 59 | -DENABLE_SHARED=${ENABLE_SHARED} 60 | -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE} 61 | -DCMAKE_PREFIX_PATH=${CMAKE_LIBRARY_OUTPUT_DIRECTORY} 62 | -DCMAKE_INSTALL_PREFIX=${CMAKE_LIBRARY_OUTPUT_DIRECTORY} 63 | -DCMAKE_INSTALL_LIBDIR=. 64 | -DCMAKE_INSTALL_INCLUDEDIR=include 65 | -DCMAKE_INSTALL_BINDIR=bin 66 | -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} 67 | -DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM} 68 | -DANDROID_TOOLCHAIN=${ANDROID_TOOLCHAIN} 69 | -DANDROID_ABI=${ANDROID_ABI} 70 | -DANDROID_PLATFORM=${ANDROID_PLATFORM} 71 | -DANDROID_STL=${ANDROID_STL} 72 | -DANDROID_PIE=${ANDROID_PIE} 73 | -DANDROID_CPP_FEATURES=${ANDROID_CPP_FEATURES} 74 | -DANDROID_ALLOW_UNDEFINED_SYMBOLS=${ANDROID_ALLOW_UNDEFINED_SYMBOLS} 75 | -DANDROID_ARM_MODE=${ANDROID_ARM_MODE} 76 | -DANDROID_DISABLE_FORMAT_STRING_CHECKS=${ANDROID_DISABLE_FORMAT_STRING_CHECKS} 77 | -DANDROID_CCACHE=${ANDROID_CCACHE} 78 | -DANDROID_SANITIZE=${ANDROID_SANITIZE} 79 | BUILD_BYPRODUCTS ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/librtmp.${LIBRARY_EXTENSION} 80 | DEPENDS crypto ssl 81 | BUILD_IN_SOURCE 1 82 | ) 83 | 84 | add_library(rtmp ${LIBRARY_FORMAT} IMPORTED) 85 | add_dependencies(rtmp rtmp_project) 86 | set_target_properties(rtmp PROPERTIES IMPORTED_LOCATION ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/librtmp.${LIBRARY_EXTENSION}) 87 | 88 | # Target library 89 | add_library(rtmpdroid SHARED glue.cpp) 90 | include_directories(${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/include) 91 | target_link_libraries(rtmpdroid log android rtmp ${TARGET_LINK_LIBRARY}) 92 | -------------------------------------------------------------------------------- /lib/src/main/cpp/Log.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #ifdef __cplusplus 6 | extern "C" { 7 | #endif 8 | 9 | #define TAG "rtmpdroid" 10 | // Log tools 11 | #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__) 12 | #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__) 13 | #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__) 14 | #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) 15 | 16 | #ifdef __cplusplus 17 | } 18 | #endif 19 | -------------------------------------------------------------------------------- /lib/src/main/cpp/glue.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "librtmp/rtmp.h" 6 | #include "librtmp/log.h" 7 | 8 | #include "models/RtmpWrapper.h" 9 | #include "Log.h" 10 | #include "models/RtmpPacket.h" 11 | 12 | #define RTMP_CLASS "video/api/rtmpdroid/Rtmp" 13 | #define AMF_ENCODER_CLASS "video/api/rtmpdroid/amf/AmfEncoder" 14 | 15 | #define STR2AVAL(av, str) av.av_val = str; av.av_len = strlen(av.av_val) 16 | 17 | JNIEXPORT jlong JNICALL 18 | nativeAlloc(JNIEnv *env, jobject thiz) { 19 | RTMP *rtmp = RTMP_Alloc(); 20 | if (rtmp == nullptr) { 21 | return 0; 22 | } 23 | RTMP_Init(rtmp); 24 | rtmp_context *rtmp_context = static_cast(malloc(sizeof(rtmp_context))); 25 | if (rtmp_context == nullptr) { 26 | return 0; 27 | } 28 | rtmp_context->rtmp = rtmp; 29 | return reinterpret_cast(rtmp_context); 30 | } 31 | 32 | JNIEXPORT jint JNICALL 33 | nativeSetupURL(JNIEnv *env, jobject thiz, jstring jurl) { 34 | rtmp_context *rtmp_context = RtmpWrapper::getNative(env, thiz); 35 | if (rtmp_context == nullptr) { 36 | return -EFAULT; 37 | } 38 | 39 | char *jvmUrl = const_cast(env->GetStringUTFChars(jurl, nullptr)); 40 | char *url = strdup(jvmUrl); 41 | STR2AVAL(rtmp_context->rtmp->Link.tcUrl, url); 42 | rtmp_context->rtmp->Link.lFlags |= RTMP_LF_FTCU; // let librtmp free tcUrl on close 43 | env->ReleaseStringUTFChars(jurl, jvmUrl); 44 | 45 | int res = RTMP_SetupURL(rtmp_context->rtmp, url); 46 | if (res == FALSE) { 47 | LOGE("Can't parse url'%s'", jvmUrl); 48 | return -1; 49 | } 50 | 51 | // Now that Link.app is set, we can compute tcUrl length 52 | rtmp_context->rtmp->Link.tcUrl.av_len = 53 | rtmp_context->rtmp->Link.app.av_len + (rtmp_context->rtmp->Link.app.av_val - url); 54 | 55 | return 0; 56 | } 57 | 58 | JNIEXPORT jint JNICALL 59 | nativeConnect(JNIEnv *env, jobject thiz) { 60 | rtmp_context *rtmp_context = RtmpWrapper::getNative(env, thiz); 61 | if (rtmp_context == nullptr) { 62 | return -EFAULT; 63 | } 64 | 65 | int res = RTMP_Connect(rtmp_context->rtmp, nullptr); 66 | if (res == FALSE) { 67 | LOGE("Can't connect"); 68 | return -1; 69 | } 70 | 71 | return 0; 72 | } 73 | 74 | JNIEXPORT jint JNICALL 75 | nativeConnectStream(JNIEnv *env, jobject thiz) { 76 | rtmp_context *rtmp_context = RtmpWrapper::getNative(env, thiz); 77 | if (rtmp_context == nullptr) { 78 | return -EFAULT; 79 | } 80 | 81 | int res = RTMP_ConnectStream(rtmp_context->rtmp, 0); 82 | if (res == FALSE) { 83 | LOGE("Can't connect stream"); 84 | return -1; 85 | } 86 | return 0; 87 | } 88 | 89 | JNIEXPORT int JNICALL 90 | nativeDeleteStream(JNIEnv *env, jobject thiz) { 91 | rtmp_context *rtmp_context = RtmpWrapper::getNative(env, thiz); 92 | if (rtmp_context == nullptr) { 93 | return -EFAULT; 94 | } 95 | 96 | RTMP_DeleteStream(rtmp_context->rtmp); 97 | return 0; 98 | } 99 | 100 | JNIEXPORT int JNICALL 101 | nativeEnableWrite(JNIEnv *env, jobject thiz) { 102 | rtmp_context *rtmp_context = RtmpWrapper::getNative(env, thiz); 103 | if (rtmp_context == nullptr) { 104 | return -EFAULT; 105 | } 106 | 107 | RTMP_EnableWrite(rtmp_context->rtmp); 108 | return 0; 109 | } 110 | 111 | JNIEXPORT jboolean JNICALL 112 | nativeIsConnected(JNIEnv *env, jobject thiz) { 113 | rtmp_context *rtmp_context = RtmpWrapper::getNative(env, thiz); 114 | if (rtmp_context == nullptr) { 115 | return false; 116 | } 117 | 118 | int isConnected = RTMP_IsConnected(rtmp_context->rtmp); 119 | return isConnected != 0; 120 | } 121 | 122 | JNIEXPORT int JNICALL 123 | nativeSetTimeout(JNIEnv *env, jobject thiz, jint timeout) { 124 | rtmp_context *rtmp_context = RtmpWrapper::getNative(env, thiz); 125 | if (rtmp_context == nullptr) { 126 | return -EFAULT; 127 | } 128 | 129 | rtmp_context->rtmp->Link.timeout = timeout; 130 | return 0; 131 | } 132 | 133 | JNIEXPORT jint JNICALL 134 | nativeGetTimeout(JNIEnv *env, jobject thiz) { 135 | rtmp_context *rtmp_context = RtmpWrapper::getNative(env, thiz); 136 | if (rtmp_context == nullptr) { 137 | return -EFAULT; 138 | } 139 | 140 | return rtmp_context->rtmp->Link.timeout; 141 | } 142 | 143 | JNIEXPORT int JNICALL 144 | nativeSetVideoCodec(JNIEnv *env, jobject thiz, jint videoCodecs) { 145 | rtmp_context *rtmp_context = RtmpWrapper::getNative(env, thiz); 146 | if (rtmp_context == nullptr) { 147 | return -EFAULT; 148 | } 149 | 150 | rtmp_context->rtmp->m_fVideoCodecs = videoCodecs; 151 | return 0; 152 | } 153 | 154 | JNIEXPORT jint JNICALL 155 | nativeGetVideoCodecs(JNIEnv *env, jobject thiz) { 156 | rtmp_context *rtmp_context = RtmpWrapper::getNative(env, thiz); 157 | if (rtmp_context == nullptr) { 158 | return -EFAULT; 159 | } 160 | 161 | return rtmp_context->rtmp->m_fVideoCodecs; 162 | } 163 | 164 | 165 | JNIEXPORT int JNICALL 166 | nativeSetExVideoCodec(JNIEnv *env, jobject thiz, jstring exVideoCodecs) { 167 | rtmp_context *rtmp_context = RtmpWrapper::getNative(env, thiz); 168 | if (rtmp_context == nullptr) { 169 | return -EFAULT; 170 | } 171 | 172 | if (exVideoCodecs == nullptr) { 173 | rtmp_context->rtmp->m_exVideoCodecs = nullptr; 174 | } else { 175 | char *ex_video_codecs = const_cast(env->GetStringUTFChars(exVideoCodecs, nullptr)); 176 | rtmp_context->rtmp->m_exVideoCodecs = strdup(ex_video_codecs); 177 | env->ReleaseStringUTFChars(exVideoCodecs, ex_video_codecs); 178 | } 179 | 180 | return 0; 181 | } 182 | 183 | JNIEXPORT jstring JNICALL 184 | nativeGetExVideoCodecs(JNIEnv *env, jobject thiz) { 185 | rtmp_context *rtmp_context = RtmpWrapper::getNative(env, thiz); 186 | if (rtmp_context == nullptr) { 187 | return nullptr; 188 | } 189 | 190 | if (rtmp_context->rtmp->m_exVideoCodecs == nullptr) { 191 | return nullptr; 192 | } else { 193 | return env->NewStringUTF(rtmp_context->rtmp->m_exVideoCodecs); 194 | } 195 | } 196 | 197 | JNIEXPORT jint JNICALL 198 | nativePause(JNIEnv *env, jobject thiz) { 199 | rtmp_context *rtmp_context = RtmpWrapper::getNative(env, thiz); 200 | if (rtmp_context == nullptr) { 201 | return -EFAULT; 202 | } 203 | 204 | return RTMP_Pause(rtmp_context->rtmp, 1); 205 | } 206 | 207 | JNIEXPORT jint JNICALL 208 | nativeResume(JNIEnv *env, jobject thiz) { 209 | rtmp_context *rtmp_context = RtmpWrapper::getNative(env, thiz); 210 | if (rtmp_context == nullptr) { 211 | return -EFAULT; 212 | } 213 | 214 | return RTMP_Pause(rtmp_context->rtmp, 0); 215 | } 216 | 217 | JNIEXPORT jint JNICALL 218 | nativeWrite(JNIEnv *env, jobject thiz, jbyteArray data, jint offset, jint size) { 219 | rtmp_context *rtmp_context = RtmpWrapper::getNative(env, thiz); 220 | if (rtmp_context == nullptr) { 221 | return -EFAULT; 222 | } 223 | 224 | char *buf = (char *) env->GetByteArrayElements(data, nullptr); 225 | 226 | int res = RTMP_Write(rtmp_context->rtmp, &buf[offset], size); 227 | 228 | env->ReleaseByteArrayElements(data, (jbyte *) buf, 0); 229 | 230 | return res; 231 | } 232 | 233 | JNIEXPORT jint JNICALL 234 | nativeWriteA(JNIEnv *env, jobject thiz, jobject buffer, jint offset, jint size) { 235 | rtmp_context *rtmp_context = RtmpWrapper::getNative(env, thiz); 236 | if (rtmp_context == nullptr) { 237 | return -EFAULT; 238 | } 239 | 240 | char *buf = (char *) env->GetDirectBufferAddress(buffer); 241 | 242 | int res = RTMP_Write(rtmp_context->rtmp, &buf[offset], size); 243 | 244 | return res; 245 | } 246 | 247 | JNIEXPORT jint JNICALL 248 | nativeRead(JNIEnv *env, jobject thiz, jbyteArray data, jint offset, jint size) { 249 | rtmp_context *rtmp_context = RtmpWrapper::getNative(env, thiz); 250 | if (rtmp_context == nullptr) { 251 | return -EFAULT; 252 | } 253 | 254 | int dataLength = env->GetArrayLength(data); 255 | int res = -1; 256 | 257 | if (dataLength >= (offset + size)) { 258 | char *buf = reinterpret_cast(env->GetByteArrayElements(data, nullptr)); 259 | res = RTMP_Read(rtmp_context->rtmp, &buf[offset], size); 260 | env->ReleaseByteArrayElements(data, reinterpret_cast(buf), 0); // 0 - free buf 261 | } 262 | 263 | return res; 264 | } 265 | 266 | JNIEXPORT jint JNICALL 267 | nativeWritePacket(JNIEnv *env, jobject thiz, jobject rtmpPacket) { 268 | rtmp_context *rtmp_context = RtmpWrapper::getNative(env, thiz); 269 | if (rtmp_context == nullptr) { 270 | return -EFAULT; 271 | } 272 | 273 | RTMPPacket *rtmp_packet = RtmpPacket::getNative(env, rtmpPacket); 274 | 275 | int res = RTMP_SendPacket(rtmp_context->rtmp, rtmp_packet, FALSE); 276 | if (res == FALSE) { 277 | LOGE("Can't write RTMP packet"); 278 | return -1; 279 | } 280 | 281 | free(rtmp_packet); 282 | 283 | return 0; 284 | } 285 | 286 | JNIEXPORT jobject JNICALL 287 | nativeReadPacket(JNIEnv *env, jobject thiz) { 288 | rtmp_context *rtmp_context = RtmpWrapper::getNative(env, thiz); 289 | if (rtmp_context == nullptr) { 290 | return nullptr; 291 | } 292 | 293 | RTMPPacket rtmp_packet = {0}; 294 | 295 | int res = RTMP_ReadPacket(rtmp_context->rtmp, &rtmp_packet); 296 | if (res == FALSE) { 297 | LOGE("Can't read RTMP packet"); 298 | return nullptr; 299 | } 300 | 301 | return RtmpPacket::getJava(env, rtmp_packet); 302 | } 303 | 304 | JNIEXPORT void JNICALL 305 | nativeClose(JNIEnv *env, jobject thiz) { 306 | rtmp_context *rtmp_context = RtmpWrapper::getNative(env, thiz); 307 | 308 | if (rtmp_context != nullptr) { 309 | if (rtmp_context->rtmp != nullptr) { 310 | RTMP_Close(rtmp_context->rtmp); 311 | RTMP_Free(rtmp_context->rtmp); 312 | rtmp_context->rtmp = nullptr; 313 | } 314 | 315 | free(rtmp_context); 316 | } 317 | } 318 | 319 | JNIEXPORT jint JNICALL 320 | nativeServe(JNIEnv *env, jobject thiz, jint fd) { 321 | rtmp_context *rtmp_context = RtmpWrapper::getNative(env, thiz); 322 | if (rtmp_context == nullptr) { 323 | return -EFAULT; 324 | } 325 | 326 | rtmp_context->rtmp->m_sb.sb_socket = fd; 327 | int ret = RTMP_Serve(rtmp_context->rtmp); 328 | if (ret == FALSE) { 329 | return -1; 330 | } else { 331 | return 0; 332 | } 333 | } 334 | 335 | 336 | static JNINativeMethod rtmpMethods[] = {{"nativeAlloc", "()J", (void *) &nativeAlloc}, 337 | {"nativeEnableWrite", "()I", (void *) &nativeEnableWrite}, 338 | {"nativeIsConnected", "()Z", (void *) &nativeIsConnected}, 339 | {"nativeSetTimeout", "(I)I", (void *) &nativeSetTimeout}, 340 | {"nativeGetTimeout", "()I", (void *) &nativeGetTimeout}, 341 | {"nativeSetVideoCodec", "(I)I", (void *) &nativeSetVideoCodec}, 342 | {"nativeGetVideoCodecs", "()I", (void *) &nativeGetVideoCodecs}, 343 | {"nativeSetExVideoCodec", "(Ljava/lang/String;)I", (void *) &nativeSetExVideoCodec}, 344 | {"nativeGetExVideoCodecs", "()Ljava/lang/String;", (void *) &nativeGetExVideoCodecs}, 345 | {"nativeSetupURL", "(Ljava/lang/String;)I", (void *) &nativeSetupURL}, 346 | {"nativeConnect", "()I", (void *) &nativeConnect}, 347 | {"nativeConnectStream", "()I", (void *) &nativeConnectStream}, 348 | {"nativeDeleteStream", "()I", (void *) &nativeDeleteStream}, 349 | {"nativePause", "()I", (void *) &nativePause}, 350 | {"nativeResume", "()I", (void *) &nativeResume}, 351 | {"nativeWrite", "([BII)I", (void *) &nativeWrite}, 352 | {"nativeWrite", "(Ljava/nio/ByteBuffer;II)I", (void *) &nativeWriteA}, 353 | {"nativeRead", "([BII)I", (void *) &nativeRead}, 354 | {"nativeWritePacket", "(L" RTMP_PACKET_CLASS";)I", (void *) &nativeWritePacket}, 355 | {"nativeReadPacket", "()L" RTMP_PACKET_CLASS";", (void *) &nativeReadPacket}, 356 | {"nativeClose", "()V", (void *) &nativeClose}, 357 | {"nativeServe", "(I)I", (void *) &nativeServe}}; 358 | 359 | JNIEXPORT jint JNICALL 360 | nativeEncodeBoolean(JNIEnv *env, jclass cls, jobject buffer, jint offset, jint end, 361 | jboolean parameter) { 362 | char *buf = (char *) env->GetDirectBufferAddress(buffer); 363 | char *newBuf = AMF_EncodeBoolean(&buf[offset], buf + end, parameter); 364 | if (nullptr == newBuf) { 365 | return -1; 366 | } else { 367 | return newBuf - buf; // new position 368 | } 369 | } 370 | 371 | JNIEXPORT jint JNICALL 372 | nativeEncodeInt24(JNIEnv *env, jclass cls, jobject buffer, jint offset, jint end, jint parameter) { 373 | char *buf = (char *) env->GetDirectBufferAddress(buffer); 374 | char *newBuf = AMF_EncodeInt24(&buf[offset], buf + end, parameter); 375 | if (nullptr == newBuf) { 376 | return -1; 377 | } else { 378 | return newBuf - buf; // new position 379 | } 380 | } 381 | 382 | JNIEXPORT jint JNICALL 383 | nativeEncodeInt(JNIEnv *env, jclass cls, jobject buffer, jint offset, jint end, jint parameter) { 384 | char *buf = (char *) env->GetDirectBufferAddress(buffer); 385 | char *newBuf = AMF_EncodeInt32(&buf[offset], buf + end, parameter); 386 | if (nullptr == newBuf) { 387 | return -1; 388 | } else { 389 | return newBuf - buf; // new position 390 | } 391 | } 392 | 393 | JNIEXPORT jint JNICALL 394 | nativeEncodeNumber(JNIEnv *env, jclass cls, jobject buffer, jint offset, jint end, 395 | jdouble parameter) { 396 | char *buf = (char *) env->GetDirectBufferAddress(buffer); 397 | char *newBuf = AMF_EncodeNumber(&buf[offset], buf + end, parameter); 398 | if (nullptr == newBuf) { 399 | return -1; 400 | } else { 401 | return newBuf - buf; // new position 402 | } 403 | } 404 | 405 | JNIEXPORT jint JNICALL 406 | nativeEncodeString(JNIEnv *env, jclass cls, jobject buffer, jint offset, jint end, 407 | jstring jparameter) { 408 | char *buf = (char *) env->GetDirectBufferAddress(buffer); 409 | char *parameter = const_cast(env->GetStringUTFChars(jparameter, nullptr)); 410 | AVal aVal = {0, 0}; 411 | STR2AVAL(aVal, parameter); 412 | char *newBuf = AMF_EncodeString(&buf[offset], buf + end, &aVal); 413 | env->ReleaseStringUTFChars(jparameter, parameter); 414 | if (nullptr == newBuf) { 415 | return -1; 416 | } else { 417 | return newBuf - buf; // new position 418 | } 419 | } 420 | 421 | JNIEXPORT jint JNICALL 422 | nativeEncodeNamedBoolean(JNIEnv *env, jclass cls, jobject buffer, jint offset, jint end, 423 | jstring jname, jboolean parameter) { 424 | char *buf = (char *) env->GetDirectBufferAddress(buffer); 425 | char *name = const_cast(env->GetStringUTFChars(jname, nullptr)); 426 | AVal av_name = {0, 0}; 427 | STR2AVAL(av_name, name); 428 | char *newBuf = AMF_EncodeNamedBoolean(&buf[offset], buf + end, &av_name, parameter); 429 | env->ReleaseStringUTFChars(jname, name); 430 | if (nullptr == newBuf) { 431 | return -1; 432 | } else { 433 | return newBuf - buf; // new position 434 | } 435 | } 436 | 437 | JNIEXPORT jint JNICALL 438 | nativeEncodeNamedNumber(JNIEnv *env, jclass cls, jobject buffer, jint offset, jint end, 439 | jstring jname, jdouble parameter) { 440 | char *buf = (char *) env->GetDirectBufferAddress(buffer); 441 | char *name = const_cast(env->GetStringUTFChars(jname, nullptr)); 442 | AVal av_name = {0, 0}; 443 | STR2AVAL(av_name, name); 444 | char *newBuf = AMF_EncodeNamedNumber(&buf[offset], buf + end, &av_name, parameter); 445 | env->ReleaseStringUTFChars(jname, name); 446 | if (nullptr == newBuf) { 447 | return -1; 448 | } else { 449 | return newBuf - buf; // new position 450 | } 451 | } 452 | 453 | JNIEXPORT jint JNICALL 454 | nativeEncodeNamedString(JNIEnv *env, jclass cls, jobject buffer, jint offset, jint end, 455 | jstring jname, jstring jparameter) { 456 | char *buf = (char *) env->GetDirectBufferAddress(buffer); 457 | char *parameter = const_cast(env->GetStringUTFChars(jparameter, nullptr)); 458 | AVal av_param = {0, 0}; 459 | STR2AVAL(av_param, parameter); 460 | char *name = const_cast(env->GetStringUTFChars(jname, nullptr)); 461 | AVal av_name = {0, 0}; 462 | STR2AVAL(av_name, name); 463 | 464 | char *newBuf = AMF_EncodeNamedString(&buf[offset], buf + end, &av_name, &av_param); 465 | env->ReleaseStringUTFChars(jname, name); 466 | env->ReleaseStringUTFChars(jparameter, parameter); 467 | if (nullptr == newBuf) { 468 | return -1; 469 | } else { 470 | return newBuf - buf; // new position 471 | } 472 | } 473 | 474 | static JNINativeMethod amfEncoderMethods[] = {{"nativeEncodeBoolean", "(Ljava/nio/ByteBuffer;IIZ)I", (void *) &nativeEncodeBoolean}, 475 | {"nativeEncodeInt", "(Ljava/nio/ByteBuffer;III)I", (void *) &nativeEncodeInt}, 476 | {"nativeEncodeInt24", "(Ljava/nio/ByteBuffer;III)I", (void *) &nativeEncodeInt24}, 477 | {"nativeEncodeNumber", "(Ljava/nio/ByteBuffer;IID)I", (void *) &nativeEncodeNumber}, 478 | {"nativeEncodeString", "(Ljava/nio/ByteBuffer;IILjava/lang/String;)I", (void *) &nativeEncodeString}, 479 | {"nativeEncodeNamedBoolean", "(Ljava/nio/ByteBuffer;IILjava/lang/String;Z)I", (void *) &nativeEncodeNamedBoolean}, 480 | {"nativeEncodeNamedNumber", "(Ljava/nio/ByteBuffer;IILjava/lang/String;D)I", (void *) &nativeEncodeNamedNumber}, 481 | {"nativeEncodeNamedString", "(Ljava/nio/ByteBuffer;IILjava/lang/String;Ljava/lang/String;)I", (void *) &nativeEncodeNamedString}}; 482 | 483 | // Register natives API 484 | 485 | static int registerNativeForClassName(JNIEnv *env, const char *className, JNINativeMethod *methods, 486 | int methodsSize) { 487 | jclass clazz = env->FindClass(className); 488 | if (clazz == nullptr) { 489 | LOGE("Unable to find class '%s'", className); 490 | return JNI_FALSE; 491 | } 492 | int res = 0; 493 | if ((res = env->RegisterNatives(clazz, methods, methodsSize)) < 0) { 494 | LOGE("RegisterNatives failed for '%s' (reason %d)", className, res); 495 | return JNI_FALSE; 496 | } 497 | 498 | return JNI_TRUE; 499 | } 500 | 501 | void rtmp_log_cb(int level, const char *format, va_list vl) { 502 | int android_log_level = ANDROID_LOG_UNKNOWN; 503 | 504 | switch (level) { 505 | case RTMP_LOGCRIT: 506 | android_log_level = ANDROID_LOG_FATAL; 507 | break; 508 | case RTMP_LOGERROR: 509 | android_log_level = ANDROID_LOG_ERROR; 510 | break; 511 | case RTMP_LOGWARNING: 512 | android_log_level = ANDROID_LOG_WARN; 513 | break; 514 | case RTMP_LOGINFO: 515 | android_log_level = ANDROID_LOG_INFO; 516 | break; 517 | case RTMP_LOGDEBUG: 518 | android_log_level = ANDROID_LOG_DEBUG; 519 | break; 520 | case RTMP_LOGDEBUG2: 521 | case RTMP_LOGALL: 522 | android_log_level = ANDROID_LOG_VERBOSE; 523 | break; 524 | default: 525 | LOGE("Unknown log level %d", level); 526 | } 527 | 528 | __android_log_vprint(android_log_level, TAG, format, vl); 529 | } 530 | 531 | jint JNI_OnLoad(JavaVM *vm, void * /*reserved*/) { 532 | JNIEnv *env = nullptr; 533 | jint result; 534 | 535 | if ((result = vm->GetEnv((void **) &env, JNI_VERSION_1_6)) != JNI_OK) { 536 | LOGE("GetEnv failed"); 537 | return result; 538 | } 539 | 540 | if ((registerNativeForClassName(env, RTMP_CLASS, rtmpMethods, 541 | sizeof(rtmpMethods) / sizeof(rtmpMethods[0])) != JNI_TRUE)) { 542 | LOGE("RegisterNatives for RTMP methods failed"); 543 | return -1; 544 | } 545 | 546 | if ((registerNativeForClassName(env, AMF_ENCODER_CLASS, amfEncoderMethods, 547 | sizeof(amfEncoderMethods) / sizeof(amfEncoderMethods[0])) != 548 | JNI_TRUE)) { 549 | LOGE("RegisterNatives for AMF encoder methods failed"); 550 | return -1; 551 | } 552 | 553 | // Register Log 554 | RTMP_LogSetCallback(rtmp_log_cb); 555 | //RTMP_LogSetLevel(RTMP_LOGDEBUG); 556 | 557 | return JNI_VERSION_1_6; 558 | } 559 | 560 | -------------------------------------------------------------------------------- /lib/src/main/cpp/models/RtmpContext.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | typedef struct rtmp_context { 4 | RTMP *rtmp; 5 | } rtmp_context; 6 | -------------------------------------------------------------------------------- /lib/src/main/cpp/models/RtmpPacket.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #define RTMP_PACKET_CLASS "video/api/rtmpdroid/RtmpPacket" 6 | 7 | class RtmpPacket { 8 | public: 9 | static RTMPPacket *getNative(JNIEnv *env, jobject rtmpPacket) { 10 | jclass rtmpPacketClz = env->GetObjectClass(rtmpPacket); 11 | if (!rtmpPacketClz) { 12 | LOGE("Can't get RtmpPacket class"); 13 | return nullptr; 14 | } 15 | 16 | jfieldID channelFieldID = env->GetFieldID(rtmpPacketClz, "channel", "I"); 17 | if (!channelFieldID) { 18 | LOGE("Can't get channel field"); 19 | env->DeleteLocalRef(rtmpPacketClz); 20 | return nullptr; 21 | } 22 | 23 | jfieldID headerTypeFieldID = env->GetFieldID(rtmpPacketClz, "headerType", "I"); 24 | if (!headerTypeFieldID) { 25 | LOGE("Can't get header type field"); 26 | env->DeleteLocalRef(rtmpPacketClz); 27 | return nullptr; 28 | } 29 | 30 | jfieldID packetTypeFieldID = env->GetFieldID(rtmpPacketClz, "packetType", "I"); 31 | if (!packetTypeFieldID) { 32 | LOGE("Can't get rtmp_packet type field"); 33 | env->DeleteLocalRef(rtmpPacketClz); 34 | return nullptr; 35 | } 36 | 37 | jfieldID timestampFieldID = env->GetFieldID(rtmpPacketClz, "timestamp", "I"); 38 | if (!timestampFieldID) { 39 | LOGE("Can't get timestamp field"); 40 | env->DeleteLocalRef(rtmpPacketClz); 41 | return nullptr; 42 | } 43 | 44 | jfieldID bufferFieldID = env->GetFieldID(rtmpPacketClz, "buffer", "Ljava/nio/ByteBuffer;"); 45 | if (!bufferFieldID) { 46 | LOGE("Can't get body field"); 47 | env->DeleteLocalRef(rtmpPacketClz); 48 | return nullptr; 49 | } 50 | 51 | RTMPPacket *rtmp_packet = static_cast(malloc(sizeof(RTMPPacket))); 52 | if (rtmp_packet == nullptr) { 53 | LOGE("Not enough memory"); 54 | env->DeleteLocalRef(rtmpPacketClz); 55 | return nullptr; 56 | } 57 | 58 | rtmp_packet->m_nChannel = env->GetIntField(rtmpPacket, channelFieldID); 59 | rtmp_packet->m_headerType = env->GetIntField(rtmpPacket, headerTypeFieldID); 60 | rtmp_packet->m_packetType = env->GetIntField(rtmpPacket, packetTypeFieldID); 61 | rtmp_packet->m_nTimeStamp = 0; 62 | rtmp_packet->m_nInfoField2 = 0; 63 | rtmp_packet->m_hasAbsTimestamp = 0; 64 | jobject buffer = env->GetObjectField(rtmpPacket, bufferFieldID); 65 | rtmp_packet->m_body = (char *) env->GetDirectBufferAddress(buffer); 66 | rtmp_packet->m_nBodySize = env->GetDirectBufferCapacity(buffer); 67 | 68 | env->DeleteLocalRef(rtmpPacketClz); 69 | 70 | return rtmp_packet; 71 | } 72 | 73 | static jobject getJava(JNIEnv *env, const RTMPPacket rtmp_packet) { 74 | jclass rtmpPacketClz = env->FindClass(RTMP_PACKET_CLASS); 75 | if (!rtmpPacketClz) { 76 | LOGE("Can't find RtmpPacket class"); 77 | return nullptr; 78 | } 79 | 80 | jmethodID rtmpPacketConstructor = env->GetMethodID(rtmpPacketClz, "", 81 | "(IIIILjava/nio/ByteBuffer;)V"); 82 | if (!rtmpPacketConstructor) { 83 | LOGE("Can't get RtmpPacket constructor"); 84 | env->DeleteLocalRef(rtmpPacketClz); 85 | return nullptr; 86 | } 87 | 88 | jobject buffer = env->NewDirectByteBuffer(rtmp_packet.m_body, rtmp_packet.m_nBodySize); 89 | jobject rtmpPacket = env->NewObject(rtmpPacketClz, rtmpPacketConstructor, 90 | rtmp_packet.m_nChannel, 91 | rtmp_packet.m_headerType, rtmp_packet.m_packetType, 92 | (int32_t) rtmp_packet.m_nTimeStamp, buffer); 93 | return rtmpPacket; 94 | } 95 | }; -------------------------------------------------------------------------------- /lib/src/main/cpp/models/RtmpWrapper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../Log.h" 4 | #include "RtmpContext.h" 5 | 6 | using namespace std; 7 | 8 | class RtmpWrapper { 9 | public: 10 | static rtmp_context *getNative(JNIEnv *env, jobject wrapperContext) { 11 | jclass wrapperContextClazz = env->GetObjectClass(wrapperContext); 12 | if (!wrapperContextClazz) { 13 | LOGE("Can't get RTMP wrapper class"); 14 | return nullptr; 15 | } 16 | 17 | jfieldID wrapperContextFieldId = env->GetFieldID(wrapperContextClazz, "ptr", "J"); 18 | if (!wrapperContextFieldId) { 19 | LOGE("Can't get ptr field"); 20 | env->DeleteLocalRef(wrapperContextClazz); 21 | return nullptr; 22 | } 23 | 24 | rtmp_context *rtmp_context = reinterpret_cast(env->GetLongField(wrapperContext, wrapperContextFieldId)); 25 | 26 | env->DeleteLocalRef(wrapperContextClazz); 27 | 28 | return rtmp_context; 29 | } 30 | }; -------------------------------------------------------------------------------- /lib/src/main/cpp/patches/0001-Port-to-openssl-1.1.1.patch: -------------------------------------------------------------------------------- 1 | From 510ef57a1106f9542e8598dc861880a2b3204f51 Mon Sep 17 00:00:00 2001 2 | From: Nirbheek Chauhan 3 | Date: Sat, 29 Feb 2020 04:06:38 +0530 4 | Subject: [PATCH 3/6] Port to openssl-1.1.1 5 | 6 | Patches by Neumann-A at vcpkg: 7 | https://github.com/microsoft/vcpkg/pull/8566/commits/6ccdbe4cb490b5dc444d96e5a0358bb5bacdfbfd 8 | 9 | Latest version of patches are: 10 | https://github.com/microsoft/vcpkg/blob/master/ports/librtmp/dh.patch 11 | https://github.com/microsoft/vcpkg/blob/master/ports/librtmp/handshake.patch 12 | https://github.com/microsoft/vcpkg/blob/master/ports/librtmp/hashswf.patch 13 | --- 14 | librtmp/dh.h | 57 ++++++++++++++++++++++++++++----------------- 15 | librtmp/handshake.h | 10 ++++---- 16 | librtmp/hashswf.c | 10 ++++---- 17 | 3 files changed, 46 insertions(+), 31 deletions(-) 18 | 19 | diff --git a/librtmp/dh.h b/librtmp/dh.h 20 | index 5fc3f32..a5dead5 100644 21 | --- a/librtmp/dh.h 22 | +++ b/librtmp/dh.h 23 | @@ -181,11 +181,14 @@ typedef BIGNUM * MP_t; 24 | #define MP_setbin(u,buf,len) BN_bn2bin(u,buf) 25 | #define MP_getbin(u,buf,len) u = BN_bin2bn(buf,len,0) 26 | 27 | + 28 | #define MDH DH 29 | #define MDH_new() DH_new() 30 | #define MDH_free(dh) DH_free(dh) 31 | #define MDH_generate_key(dh) DH_generate_key(dh) 32 | #define MDH_compute_key(secret, seclen, pub, dh) DH_compute_key(secret, pub, dh) 33 | +#define MPH_set_pqg(dh, p, q, g, res) res = DH_set0_pqg(dh, p, q, g) 34 | +#define MPH_set_length(dh, len, res) res = DH_set_length(dh,len) 35 | 36 | #endif 37 | 38 | @@ -194,7 +197,7 @@ typedef BIGNUM * MP_t; 39 | 40 | /* RFC 2631, Section 2.1.5, http://www.ietf.org/rfc/rfc2631.txt */ 41 | static int 42 | -isValidPublicKey(MP_t y, MP_t p, MP_t q) 43 | +isValidPublicKey(const MP_t y,const MP_t p, MP_t q) 44 | { 45 | int ret = TRUE; 46 | MP_t bn; 47 | @@ -253,20 +256,33 @@ DHInit(int nKeyBits) 48 | if (!dh) 49 | goto failed; 50 | 51 | - MP_new(dh->g); 52 | + MP_t g,p; 53 | + MP_new(g); 54 | 55 | - if (!dh->g) 56 | + if (!g) 57 | + { 58 | goto failed; 59 | + } 60 | 61 | - MP_gethex(dh->p, P1024, res); /* prime P1024, see dhgroups.h */ 62 | + DH_get0_pqg(dh, (BIGNUM const**)&p, NULL, NULL); 63 | + MP_gethex(p, P1024, res); /* prime P1024, see dhgroups.h */ 64 | if (!res) 65 | { 66 | goto failed; 67 | } 68 | 69 | - MP_set_w(dh->g, 2); /* base 2 */ 70 | - 71 | - dh->length = nKeyBits; 72 | + MP_set_w(g, 2); /* base 2 */ 73 | + MPH_set_pqg(dh,p,NULL,g, res); 74 | + if (!res) 75 | + { 76 | + MP_free(g); 77 | + goto failed; 78 | + } 79 | + MPH_set_length(dh,nKeyBits, res); 80 | + if (!res) 81 | + { 82 | + goto failed; 83 | + } 84 | return dh; 85 | 86 | failed: 87 | @@ -292,14 +308,11 @@ DHGenerateKey(MDH *dh) 88 | 89 | MP_gethex(q1, Q1024, res); 90 | assert(res); 91 | - 92 | - res = isValidPublicKey(dh->pub_key, dh->p, q1); 93 | + res = isValidPublicKey(DH_get0_pub_key(dh), DH_get0_p(dh), q1); 94 | if (!res) 95 | - { 96 | - MP_free(dh->pub_key); 97 | - MP_free(dh->priv_key); 98 | - dh->pub_key = dh->priv_key = 0; 99 | - } 100 | + { 101 | + MDH_free(dh); // Cannot set priv_key to nullptr so there is no way to generate a new pub/priv key pair in openssl 1.1.1. 102 | + } 103 | 104 | MP_free(q1); 105 | } 106 | @@ -314,15 +327,16 @@ static int 107 | DHGetPublicKey(MDH *dh, uint8_t *pubkey, size_t nPubkeyLen) 108 | { 109 | int len; 110 | - if (!dh || !dh->pub_key) 111 | + MP_t pub = DH_get0_pub_key(dh); 112 | + if (!dh || !pub) 113 | return 0; 114 | 115 | - len = MP_bytes(dh->pub_key); 116 | + len = MP_bytes(pub); 117 | if (len <= 0 || len > (int) nPubkeyLen) 118 | return 0; 119 | 120 | memset(pubkey, 0, nPubkeyLen); 121 | - MP_setbin(dh->pub_key, pubkey + (nPubkeyLen - len), len); 122 | + MP_setbin(pub, pubkey + (nPubkeyLen - len), len); 123 | return 1; 124 | } 125 | 126 | @@ -330,15 +344,16 @@ DHGetPublicKey(MDH *dh, uint8_t *pubkey, size_t nPubkeyLen) 127 | static int 128 | DHGetPrivateKey(MDH *dh, uint8_t *privkey, size_t nPrivkeyLen) 129 | { 130 | - if (!dh || !dh->priv_key) 131 | + MP_t priv = DH_get0_priv_key(dh); 132 | + if (!dh || !priv) 133 | return 0; 134 | 135 | - int len = MP_bytes(dh->priv_key); 136 | + int len = MP_bytes(priv); 137 | if (len <= 0 || len > (int) nPrivkeyLen) 138 | return 0; 139 | 140 | memset(privkey, 0, nPrivkeyLen); 141 | - MP_setbin(dh->priv_key, privkey + (nPrivkeyLen - len), len); 142 | + MP_setbin(priv, privkey + (nPrivkeyLen - len), len); 143 | return 1; 144 | } 145 | #endif 146 | @@ -364,7 +379,7 @@ DHComputeSharedSecretKey(MDH *dh, uint8_t *pubkey, size_t nPubkeyLen, 147 | MP_gethex(q1, Q1024, len); 148 | assert(len); 149 | 150 | - if (isValidPublicKey(pubkeyBn, dh->p, q1)) 151 | + if (isValidPublicKey(pubkeyBn, DH_get0_p(dh), q1)) 152 | res = MDH_compute_key(secret, nPubkeyLen, pubkeyBn, dh); 153 | else 154 | res = -1; 155 | diff --git a/librtmp/handshake.h b/librtmp/handshake.h 156 | index 0438486..5313943 100644 157 | --- a/librtmp/handshake.h 158 | +++ b/librtmp/handshake.h 159 | @@ -69,9 +69,9 @@ typedef struct arcfour_ctx* RC4_handle; 160 | #if OPENSSL_VERSION_NUMBER < 0x0090800 || !defined(SHA256_DIGEST_LENGTH) 161 | #error Your OpenSSL is too old, need 0.9.8 or newer with SHA256 162 | #endif 163 | -#define HMAC_setup(ctx, key, len) HMAC_CTX_init(&ctx); HMAC_Init_ex(&ctx, key, len, EVP_sha256(), 0) 164 | -#define HMAC_crunch(ctx, buf, len) HMAC_Update(&ctx, buf, len) 165 | -#define HMAC_finish(ctx, dig, dlen) HMAC_Final(&ctx, dig, &dlen); HMAC_CTX_cleanup(&ctx) 166 | +#define HMAC_setup(ctx, key, len) ctx = HMAC_CTX_new(); HMAC_Init_ex(ctx, key, len, EVP_sha256(), 0) 167 | +#define HMAC_crunch(ctx, buf, len) HMAC_Update(ctx, buf, len) 168 | +#define HMAC_finish(ctx, dig, dlen) HMAC_Final(ctx, dig, &dlen); HMAC_CTX_free(ctx) 169 | 170 | typedef RC4_KEY * RC4_handle; 171 | #define RC4_alloc(h) *h = malloc(sizeof(RC4_KEY)) 172 | @@ -117,7 +117,7 @@ static void InitRC4Encryption 173 | { 174 | uint8_t digest[SHA256_DIGEST_LENGTH]; 175 | unsigned int digestLen = 0; 176 | - HMAC_CTX ctx; 177 | + HMAC_CTX *ctx; 178 | 179 | RC4_alloc(rc4keyIn); 180 | RC4_alloc(rc4keyOut); 181 | @@ -266,7 +266,7 @@ HMACsha256(const uint8_t *message, size_t messageLen, const uint8_t *key, 182 | size_t keylen, uint8_t *digest) 183 | { 184 | unsigned int digestLen; 185 | - HMAC_CTX ctx; 186 | + HMAC_CTX *ctx; 187 | 188 | HMAC_setup(ctx, key, keylen); 189 | HMAC_crunch(ctx, message, messageLen); 190 | diff --git a/librtmp/hashswf.c b/librtmp/hashswf.c 191 | index 32b2eed..537e571 100644 192 | --- a/librtmp/hashswf.c 193 | +++ b/librtmp/hashswf.c 194 | @@ -57,10 +57,10 @@ 195 | #include 196 | #include 197 | #include 198 | -#define HMAC_setup(ctx, key, len) HMAC_CTX_init(&ctx); HMAC_Init_ex(&ctx, (unsigned char *)key, len, EVP_sha256(), 0) 199 | -#define HMAC_crunch(ctx, buf, len) HMAC_Update(&ctx, (unsigned char *)buf, len) 200 | -#define HMAC_finish(ctx, dig, dlen) HMAC_Final(&ctx, (unsigned char *)dig, &dlen); 201 | -#define HMAC_close(ctx) HMAC_CTX_cleanup(&ctx) 202 | +#define HMAC_setup(ctx, key, len) ctx = HMAC_CTX_new(); HMAC_Init_ex(ctx, (unsigned char *)key, len, EVP_sha256(), 0) 203 | +#define HMAC_crunch(ctx, buf, len) HMAC_Update(ctx, (unsigned char *)buf, len) 204 | +#define HMAC_finish(ctx, dig, dlen) HMAC_Final(ctx, (unsigned char *)dig, &dlen); 205 | +#define HMAC_close(ctx) HMAC_CTX_free(ctx) 206 | #endif 207 | 208 | extern void RTMP_TLS_Init(); 209 | @@ -298,7 +298,7 @@ leave: 210 | struct info 211 | { 212 | z_stream *zs; 213 | - HMAC_CTX ctx; 214 | + HMAC_CTX *ctx; 215 | int first; 216 | int zlib; 217 | int size; 218 | -- 219 | 2.24.1 220 | 221 | -------------------------------------------------------------------------------- /lib/src/main/cpp/patches/0002-Add-CMakeLists.txt.patch: -------------------------------------------------------------------------------- 1 | From a04498ccf4b69572ea86912a8a0010d6d8896bce Mon Sep 17 00:00:00 2001 2 | From: ThibaultBee 3 | Date: Fri, 11 Feb 2022 16:52:29 +0100 4 | Subject: [PATCH] Add CMakeLists.txt 5 | 6 | --- 7 | CMakeLists.txt | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++ 8 | 1 file changed, 81 insertions(+) 9 | create mode 100644 CMakeLists.txt 10 | 11 | diff --git a/CMakeLists.txt b/CMakeLists.txt 12 | new file mode 100644 13 | index 0000000..2fdfe91 14 | --- /dev/null 15 | +++ b/CMakeLists.txt 16 | @@ -0,0 +1,81 @@ 17 | +cmake_minimum_required(VERSION 3.6 FATAL_ERROR) 18 | + 19 | +project(RTMP VERSION "2.4" LANGUAGES C) 20 | +add_definitions(-DRTMPDUMP_VERSION="v${PROJECT_VERSION}") 21 | + 22 | +option(ENABLE_EXAMPLES "Should the example be built?" ON) 23 | +option(ENABLE_SHARED "Should librtmp be built as a shared library" ON) 24 | +option(ENABLE_STATIC "Should librtmp be built as a static library" ON) 25 | +set(CRYPTO OPENSSL CACHE STRING "Set crypto library") 26 | +set_property(CACHE CRYPTO PROPERTY STRINGS OPENSSL GNUTLS POLARSSL) 27 | + 28 | +# librtmp 29 | +file(GLOB SOURCES ./librtmp/*.c) 30 | +file(GLOB HEADERS ./librtmp/rtmp.h ./librtmp/amf.h ./librtmp/log.h) 31 | +if(ENABLE_SHARED) 32 | + add_library(rtmp_shared SHARED ${SOURCES}) 33 | + set_property(TARGET rtmp_shared PROPERTY OUTPUT_NAME rtmp) 34 | + list(APPEND INSTALL_TARGETS rtmp_shared) 35 | +endif() 36 | +if(ENABLE_STATIC) 37 | + add_library(rtmp_static STATIC ${SOURCES}) 38 | + set_property(TARGET rtmp_static PROPERTY OUTPUT_NAME rtmp) 39 | + list(APPEND INSTALL_TARGETS rtmp_static) 40 | +endif() 41 | + 42 | + 43 | +find_package(ZLIB REQUIRED) 44 | +if(CRYPTO STREQUAL "POLARSSL") 45 | + find_package(PolarSSL REQUIRED) 46 | + set(SSL_LIBRARIES ${POLARSSL_LIBRARIES}) 47 | + set(SSL_INCLUDE_DIRS ${POLARSSL_INCLUDE_DIR}) 48 | +elseif(CRYPTO STREQUAL "GNUTLS") 49 | + find_package(GnuTLS REQUIRED) 50 | + set(SSL_LIBRARIES ${GNUTLS_LIBRARIES}) 51 | + set(SSL_INCLUDE_DIRS ${GNUTLS_INCLUDE_DIR}) 52 | +elseif(CRYPTO STREQUAL "OPENSSL") 53 | + find_package(OpenSSL REQUIRED) 54 | + set(SSL_LIBRARIES ${OPENSSL_LIBRARIES}) 55 | + set(SSL_INCLUDE_DIRS ${OPENSSL_INCLUDE_DIR}) 56 | +else() 57 | + message(FATAL_ERROR "Unknown crypto lib") 58 | +endif() 59 | + 60 | +if(ENABLE_SHARED) 61 | + target_include_directories(rtmp_shared PRIVATE ${SSL_INCLUDE_DIRS}) 62 | + target_link_libraries(rtmp_shared PRIVATE ${SSL_LIBRARIES} ${ZLIB_LIBRARIES}) 63 | +endif() 64 | +if(ENABLE_STATIC) 65 | + target_include_directories(rtmp_static PRIVATE ${SSL_INCLUDE_DIRS}) 66 | + target_link_libraries(rtmp_static PRIVATE ${SSL_LIBRARIES} ${ZLIB_LIBRARIES}) 67 | +endif() 68 | + 69 | +# Examples 70 | +if(ENABLE_EXAMPLES) 71 | + if(ENABLE_STATIC) 72 | + set(LINK_LIBRARY rtmp_static) 73 | + elseif(ENABLE_SHARED) 74 | + set(LINK_LIBRARY rtmp_shared) 75 | + endif() 76 | + 77 | + add_executable(rtmpdump rtmpdump.c) 78 | + target_link_libraries(rtmpdump ${LINK_LIBRARY}) 79 | + 80 | + add_executable(rtmpsrv rtmpsrv.c thread.c) 81 | + target_link_libraries(rtmpsrv ${LINK_LIBRARY}) 82 | + 83 | + add_executable(rtmpsuck rtmpsuck.c thread.c) 84 | + target_link_libraries(rtmpsuck ${LINK_LIBRARY}) 85 | + 86 | + add_executable(rtmpgw rtmpgw.c thread.c) 87 | + target_link_libraries(rtmpgw ${LINK_LIBRARY}) 88 | + list (APPEND INSTALL_TARGETS rtmpdump rtmpsrv rtmpsuck rtmpgw) 89 | +endif() 90 | + 91 | +# Install 92 | +install(TARGETS ${INSTALL_TARGETS} 93 | + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} 94 | + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} 95 | + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} 96 | +) 97 | +install(FILES ${HEADERS} DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/librtmp) 98 | \ No newline at end of file 99 | -- 100 | 2.34.1 101 | 102 | -------------------------------------------------------------------------------- /lib/src/main/cpp/patches/0003-Fix-AMF_EncodeString-size-check.patch: -------------------------------------------------------------------------------- 1 | From 1589af4e44e567596d2b980db61a4a7a5a8b611d Mon Sep 17 00:00:00 2001 2 | From: ThibaultBee 3 | Date: Thu, 10 Mar 2022 10:23:31 +0100 4 | Subject: [PATCH] Fix AMF_EncodeString size check 5 | 6 | --- 7 | librtmp/amf.c | 2 +- 8 | 1 file changed, 1 insertion(+), 1 deletion(-) 9 | 10 | diff --git a/librtmp/amf.c b/librtmp/amf.c 11 | index 7954144..2644624 100644 12 | --- a/librtmp/amf.c 13 | +++ b/librtmp/amf.c 14 | @@ -174,7 +174,7 @@ char * 15 | AMF_EncodeString(char *output, char *outend, const AVal *bv) 16 | { 17 | if ((bv->av_len < 65536 && output + 1 + 2 + bv->av_len > outend) || 18 | - output + 1 + 4 + bv->av_len > outend) 19 | + (bv->av_len >= 65536 && output + 1 + 4 + bv->av_len > outend)) 20 | return NULL; 21 | 22 | if (bv->av_len < 65536) 23 | -- 24 | 2.34.1 25 | 26 | -------------------------------------------------------------------------------- /lib/src/main/cpp/patches/0004-Modernize-socket-API-usage.patch: -------------------------------------------------------------------------------- 1 | From edea9d7c79321b57b072f0ca10fdb2a4a669dc30 Mon Sep 17 00:00:00 2001 2 | From: ThibaultBee 3 | Date: Wed, 16 Mar 2022 16:24:29 +0100 4 | Subject: [PATCH] Modernize socket API usage 5 | 6 | --- 7 | librtmp/rtmp.c | 65 ++++++++++++++++++++++++++++++-------------------- 8 | librtmp/rtmp.h | 2 +- 9 | 2 files changed, 40 insertions(+), 27 deletions(-) 10 | 11 | diff --git a/librtmp/rtmp.c b/librtmp/rtmp.c 12 | index 0865689..43476ee 100644 13 | --- a/librtmp/rtmp.c 14 | +++ b/librtmp/rtmp.c 15 | @@ -28,6 +28,7 @@ 16 | #include 17 | #include 18 | #include 19 | +#include 20 | 21 | #include "rtmp_sys.h" 22 | #include "log.h" 23 | @@ -867,7 +868,7 @@ int RTMP_SetupURL(RTMP *r, char *url) 24 | } 25 | 26 | static int 27 | -add_addr_info(struct sockaddr_in *service, AVal *host, int port) 28 | +add_addr_info(struct sockaddr *service, int *service_size, AVal *host, int port) 29 | { 30 | char *hostname; 31 | int ret = TRUE; 32 | @@ -882,20 +883,29 @@ add_addr_info(struct sockaddr_in *service, AVal *host, int port) 33 | hostname = host->av_val; 34 | } 35 | 36 | - service->sin_addr.s_addr = inet_addr(hostname); 37 | - if (service->sin_addr.s_addr == INADDR_NONE) 38 | - { 39 | - struct hostent *host = gethostbyname(hostname); 40 | - if (host == NULL || host->h_addr == NULL) 41 | - { 42 | - RTMP_Log(RTMP_LOGERROR, "Problem accessing the DNS. (addr: %s)", hostname); 43 | - ret = FALSE; 44 | - goto finish; 45 | - } 46 | - service->sin_addr = *(struct in_addr *)host->h_addr; 47 | + // Get hostname type: IPv4 or IPv6 48 | + struct addrinfo hint = {0}; 49 | + struct addrinfo *ai = NULL; 50 | + hint.ai_family = PF_UNSPEC; 51 | + // hint.ai_flags = AI_NUMERICHOST | AI_ADDRCONFIG; 52 | + char aiservice[11] = {0}; 53 | + sprintf(aiservice, "%d", port); 54 | + 55 | + if (getaddrinfo(hostname, aiservice, &hint, &ai)) { 56 | + ret = FALSE; 57 | + goto finish; 58 | } 59 | 60 | - service->sin_port = htons(port); 61 | + if ((ai->ai_family != AF_INET) && (ai->ai_family != AF_INET6)) { 62 | + freeaddrinfo(ai); 63 | + ret = FALSE; 64 | + goto finish; 65 | + } 66 | + 67 | + *service_size = ai->ai_addrlen; 68 | + memcpy(service, ai->ai_addr, ai->ai_addrlen); 69 | + 70 | + freeaddrinfo(ai); 71 | finish: 72 | if (hostname != host->av_val) 73 | free(hostname); 74 | @@ -903,17 +913,17 @@ finish: 75 | } 76 | 77 | int 78 | -RTMP_Connect0(RTMP *r, struct sockaddr * service) 79 | +RTMP_Connect0(RTMP *r, struct sockaddr * service, int service_size) 80 | { 81 | int on = 1; 82 | r->m_sb.sb_timedout = FALSE; 83 | r->m_pausing = 0; 84 | r->m_fDuration = 0.0; 85 | 86 | - r->m_sb.sb_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); 87 | + r->m_sb.sb_socket = socket(service->sa_family, SOCK_STREAM, IPPROTO_TCP); 88 | if (r->m_sb.sb_socket != -1) 89 | { 90 | - if (connect(r->m_sb.sb_socket, service, sizeof(struct sockaddr)) < 0) 91 | + if (connect(r->m_sb.sb_socket, (struct sockaddr *)service, service_size) < 0) 92 | { 93 | int err = GetSockError(); 94 | RTMP_Log(RTMP_LOGERROR, "%s, failed to connect socket. %d (%s)", 95 | @@ -1030,27 +1040,27 @@ RTMP_Connect1(RTMP *r, RTMPPacket *cp) 96 | int 97 | RTMP_Connect(RTMP *r, RTMPPacket *cp) 98 | { 99 | - struct sockaddr_in service; 100 | + struct sockaddr_storage service; 101 | + int service_size = 0; 102 | if (!r->Link.hostname.av_len) 103 | return FALSE; 104 | 105 | - memset(&service, 0, sizeof(struct sockaddr_in)); 106 | - service.sin_family = AF_INET; 107 | + memset(&service, 0, sizeof(struct sockaddr_storage)); 108 | 109 | if (r->Link.socksport) 110 | { 111 | /* Connect via SOCKS */ 112 | - if (!add_addr_info(&service, &r->Link.sockshost, r->Link.socksport)) 113 | + if (!add_addr_info((struct sockaddr *)&service, &service_size, &r->Link.sockshost, r->Link.socksport)) 114 | return FALSE; 115 | } 116 | else 117 | { 118 | /* Connect directly */ 119 | - if (!add_addr_info(&service, &r->Link.hostname, r->Link.port)) 120 | + if (!add_addr_info((struct sockaddr *)&service, &service_size, &r->Link.hostname, r->Link.port)) 121 | return FALSE; 122 | } 123 | 124 | - if (!RTMP_Connect0(r, (struct sockaddr *)&service)) 125 | + if (!RTMP_Connect0(r, (struct sockaddr *)&service, service_size)) 126 | return FALSE; 127 | 128 | r->m_bSendCounter = TRUE; 129 | @@ -1062,11 +1072,14 @@ static int 130 | SocksNegotiate(RTMP *r) 131 | { 132 | unsigned long addr; 133 | - struct sockaddr_in service; 134 | - memset(&service, 0, sizeof(struct sockaddr_in)); 135 | + struct sockaddr_storage service; 136 | + int service_size = 0; 137 | + memset(&service, 0, sizeof(struct sockaddr_storage)); 138 | 139 | - add_addr_info(&service, &r->Link.hostname, r->Link.port); 140 | - addr = htonl(service.sin_addr.s_addr); 141 | + add_addr_info((struct sockaddr *)&service, &service_size, &r->Link.hostname, r->Link.port); 142 | + if (service.ss_family == AF_INET) { 143 | + // addr = htonl(((struct sockaddr_in)service).sin_addr.s_addr) 144 | + } 145 | 146 | { 147 | char packet[] = { 148 | diff --git a/librtmp/rtmp.h b/librtmp/rtmp.h 149 | index 6d7dd89..e7c6cf2 100644 150 | --- a/librtmp/rtmp.h 151 | +++ b/librtmp/rtmp.h 152 | @@ -311,7 +311,7 @@ extern "C" 153 | 154 | int RTMP_Connect(RTMP *r, RTMPPacket *cp); 155 | struct sockaddr; 156 | - int RTMP_Connect0(RTMP *r, struct sockaddr *svc); 157 | + int RTMP_Connect0(RTMP *r, struct sockaddr * service, int service_size); 158 | int RTMP_Connect1(RTMP *r, RTMPPacket *cp); 159 | int RTMP_Serve(RTMP *r); 160 | int RTMP_TLS_Accept(RTMP *r, void *ctx); 161 | -- 162 | 2.34.1 163 | 164 | -------------------------------------------------------------------------------- /lib/src/main/cpp/patches/0005-Shutdown-socket-on-close-to-interrupt-socket-connect.patch: -------------------------------------------------------------------------------- 1 | From 54040b6508d8a4ff268cc6661c0e6ac8993763bf Mon Sep 17 00:00:00 2001 2 | From: ThibaultBee 3 | Date: Thu, 4 Aug 2022 17:01:47 +0200 4 | Subject: [PATCH] Shutdown socket on close to interrupt socket connection 5 | 6 | --- 7 | librtmp/rtmp_sys.h | 3 ++- 8 | 1 file changed, 2 insertions(+), 1 deletion(-) 9 | 10 | diff --git a/librtmp/rtmp_sys.h b/librtmp/rtmp_sys.h 11 | index 85d7e53..d9512c0 100644 12 | --- a/librtmp/rtmp_sys.h 13 | +++ b/librtmp/rtmp_sys.h 14 | @@ -53,7 +53,8 @@ 15 | #define GetSockError() errno 16 | #define SetSockError(e) errno = e 17 | #undef closesocket 18 | -#define closesocket(s) close(s) 19 | +#define closesocket(s) shutdown(s, SHUT_RDWR);\ 20 | + close(s) 21 | #define msleep(n) usleep(n*1000) 22 | #define SET_RCVTIMEO(tv,s) struct timeval tv = {s,0} 23 | #endif 24 | -- 25 | 2.34.1 26 | 27 | -------------------------------------------------------------------------------- /lib/src/main/cpp/patches/0006-Add-support-for-enhanced-RTMP.patch: -------------------------------------------------------------------------------- 1 | From 7c9c09619697ca548829e85c99c8447fc628f298 Mon Sep 17 00:00:00 2001 2 | From: ThibaultBee <37510686+ThibaultBee@users.noreply.github.com> 3 | Date: Mon, 16 Oct 2023 21:57:55 +0200 4 | Subject: [PATCH] Add support for enhanced RTMP 5 | 6 | --- 7 | librtmp/amf.c | 13 +++++++++++++ 8 | librtmp/amf.h | 1 + 9 | librtmp/rtmp.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 10 | librtmp/rtmp.h | 1 + 11 | rtmpsrv.c | 1 + 12 | 5 files changed, 62 insertions(+) 13 | 14 | diff --git a/librtmp/amf.c b/librtmp/amf.c 15 | index 2644624..1f6b267 100644 16 | --- a/librtmp/amf.c 17 | +++ b/librtmp/amf.c 18 | @@ -308,6 +308,19 @@ AMF_EncodeNamedBoolean(char *output, char *outend, const AVal *strName, int bVal 19 | return AMF_EncodeBoolean(output, outend, bVal); 20 | } 21 | 22 | +char * 23 | +AMF_EncodeNamedArray(char *output, char *outend, const AVal *strName, AMFObject *obj) 24 | +{ 25 | + if (output+2+strName->av_len > outend) 26 | + return NULL; 27 | + output = AMF_EncodeInt16(output, outend, strName->av_len); 28 | + 29 | + memcpy(output, strName->av_val, strName->av_len); 30 | + output += strName->av_len; 31 | + 32 | + return AMF_EncodeArray(obj, output, outend); 33 | +} 34 | + 35 | void 36 | AMFProp_GetName(AMFObjectProperty *prop, AVal *name) 37 | { 38 | diff --git a/librtmp/amf.h b/librtmp/amf.h 39 | index 5de414b..627fd74 100644 40 | --- a/librtmp/amf.h 41 | +++ b/librtmp/amf.h 42 | @@ -94,6 +94,7 @@ extern "C" 43 | char *AMF_EncodeNamedString(char *output, char *outend, const AVal * name, const AVal * value); 44 | char *AMF_EncodeNamedNumber(char *output, char *outend, const AVal * name, double dVal); 45 | char *AMF_EncodeNamedBoolean(char *output, char *outend, const AVal * name, int bVal); 46 | + char *AMF_EncodeNamedArray(char *output, char *outend, const AVal *strName, AMFObject *obj); 47 | 48 | unsigned short AMF_DecodeInt16(const char *data); 49 | unsigned int AMF_DecodeInt24(const char *data); 50 | diff --git a/librtmp/rtmp.c b/librtmp/rtmp.c 51 | index a8f6ac1..20a27eb 100644 52 | --- a/librtmp/rtmp.c 53 | +++ b/librtmp/rtmp.c 54 | @@ -1581,6 +1581,7 @@ SAVC(fpad); 55 | SAVC(capabilities); 56 | SAVC(audioCodecs); 57 | SAVC(videoCodecs); 58 | +SAVC(fourCcList); 59 | SAVC(videoFunction); 60 | SAVC(objectEncoding); 61 | SAVC(secureToken); 62 | @@ -1588,6 +1589,38 @@ SAVC(secureTokenResponse); 63 | SAVC(type); 64 | SAVC(nonprivate); 65 | 66 | + 67 | +static char *EncodeFourCCList(char *enc, char *pend, char *exVideoCodecs) 68 | +{ 69 | + char *fourCC = exVideoCodecs; 70 | + int fourCCLen = strlen(exVideoCodecs); 71 | + AMFObjectProperty p = {{0,0}}; 72 | + AMFObject obj; 73 | + obj.o_num = 0; 74 | + obj.o_props = NULL; 75 | + 76 | + if (!exVideoCodecs) { 77 | + // No need to encode anything 78 | + return enc; 79 | + } 80 | + 81 | + while(fourCC - exVideoCodecs < fourCCLen) { 82 | + p.p_type = AMF_STRING; 83 | + p.p_vu.p_aval.av_val = fourCC; 84 | + p.p_vu.p_aval.av_len = 4; 85 | + 86 | + AMF_AddProp(&obj, &p); 87 | + 88 | + fourCC += 5; 89 | + } 90 | + 91 | + enc = AMF_EncodeNamedArray(enc, pend, &av_fourCcList, &obj); 92 | + AMF_Reset(&obj); 93 | + 94 | + return enc; 95 | +} 96 | + 97 | + 98 | static int 99 | SendConnectPacket(RTMP *r, RTMPPacket *cp) 100 | { 101 | @@ -1668,6 +1701,14 @@ SendConnectPacket(RTMP *r, RTMPPacket *cp) 102 | if (!enc) 103 | return FALSE; 104 | } 105 | + // Add enhanced video codecs 106 | + if (r->m_exVideoCodecs) 107 | + { 108 | + enc = EncodeFourCCList(enc, pend, r->m_exVideoCodecs); 109 | + if (!enc) 110 | + return FALSE; 111 | + } 112 | + 113 | if (enc + 3 >= pend) 114 | return FALSE; 115 | *enc++ = 0; 116 | @@ -4259,6 +4300,11 @@ CloseInternal(RTMP *r, int reconnect) 117 | r->Link.rc4keyOut = NULL; 118 | } 119 | #endif 120 | + if (r->m_exVideoCodecs) 121 | + { 122 | + free(r->m_exVideoCodecs); 123 | + r->m_exVideoCodecs = NULL; 124 | + } 125 | } 126 | 127 | int 128 | diff --git a/librtmp/rtmp.h b/librtmp/rtmp.h 129 | index e7c6cf2..424dc48 100644 130 | --- a/librtmp/rtmp.h 131 | +++ b/librtmp/rtmp.h 132 | @@ -266,6 +266,7 @@ extern "C" 133 | 134 | double m_fAudioCodecs; /* audioCodecs for the connect packet */ 135 | double m_fVideoCodecs; /* videoCodecs for the connect packet */ 136 | + char *m_exVideoCodecs; /* fourCcList for the connect packet for enhanced RTMP. Expect a string with format: `hvc1[,av01][,vp09][,...]\n` */ 137 | double m_fEncoding; /* AMF0 or AMF3 */ 138 | 139 | double m_fDuration; /* duration of stream in seconds */ 140 | diff --git a/rtmpsrv.c b/rtmpsrv.c 141 | index 5df4d3a..721fe19 100644 142 | --- a/rtmpsrv.c 143 | +++ b/rtmpsrv.c 144 | @@ -156,6 +156,7 @@ SAVC(fpad); 145 | SAVC(capabilities); 146 | SAVC(audioCodecs); 147 | SAVC(videoCodecs); 148 | +SAVC(fourCcList); 149 | SAVC(videoFunction); 150 | SAVC(objectEncoding); 151 | SAVC(_result); 152 | -- 153 | 2.39.1 154 | 155 | -------------------------------------------------------------------------------- /lib/src/main/cpp/patches/0007-When-packet-are-not-in-order-force-the-header-of-typ.patch: -------------------------------------------------------------------------------- 1 | From 077dab84ddc15fabe7b0f9955f3fc0d33be1e56d Mon Sep 17 00:00:00 2001 2 | From: ThibaultBee <37510686+ThibaultBee@users.noreply.github.com> 3 | Date: Wed, 3 Jan 2024 10:12:39 +0100 4 | Subject: [PATCH] When packet are not in order, force the header of type 0 5 | 6 | --- 7 | librtmp/rtmp.c | 28 ++++++++++++++++++---------- 8 | 1 file changed, 18 insertions(+), 10 deletions(-) 9 | 10 | diff --git a/librtmp/rtmp.c b/librtmp/rtmp.c 11 | index 20a27eb..ebdfc85 100644 12 | --- a/librtmp/rtmp.c 13 | +++ b/librtmp/rtmp.c 14 | @@ -3977,16 +3977,24 @@ RTMP_SendPacket(RTMP *r, RTMPPacket *packet, int queue) 15 | prevPacket = r->m_vecChannelsOut[packet->m_nChannel]; 16 | if (prevPacket && packet->m_headerType != RTMP_PACKET_SIZE_LARGE) 17 | { 18 | - /* compress a bit by using the prev packet's attributes */ 19 | - if (prevPacket->m_nBodySize == packet->m_nBodySize 20 | - && prevPacket->m_packetType == packet->m_packetType 21 | - && packet->m_headerType == RTMP_PACKET_SIZE_MEDIUM) 22 | - packet->m_headerType = RTMP_PACKET_SIZE_SMALL; 23 | - 24 | - if (prevPacket->m_nTimeStamp == packet->m_nTimeStamp 25 | - && packet->m_headerType == RTMP_PACKET_SIZE_SMALL) 26 | - packet->m_headerType = RTMP_PACKET_SIZE_MINIMUM; 27 | - last = prevPacket->m_nTimeStamp; 28 | + if (packet->m_nTimeStamp < prevPacket->m_nTimeStamp) 29 | + { 30 | + /* if packet are going backward, we force header type 0 */ 31 | + packet->m_headerType = RTMP_PACKET_SIZE_LARGE; 32 | + } 33 | + else 34 | + { 35 | + /* compress a bit by using the prev packet's attributes */ 36 | + if (prevPacket->m_nBodySize == packet->m_nBodySize 37 | + && prevPacket->m_packetType == packet->m_packetType 38 | + && packet->m_headerType == RTMP_PACKET_SIZE_MEDIUM) 39 | + packet->m_headerType = RTMP_PACKET_SIZE_SMALL; 40 | + 41 | + if (prevPacket->m_nTimeStamp == packet->m_nTimeStamp 42 | + && packet->m_headerType == RTMP_PACKET_SIZE_SMALL) 43 | + packet->m_headerType = RTMP_PACKET_SIZE_MINIMUM; 44 | + last = prevPacket->m_nTimeStamp; 45 | + } 46 | } 47 | 48 | if (packet->m_headerType > 3) /* sanity */ 49 | -- 50 | 2.39.1 51 | 52 | -------------------------------------------------------------------------------- /lib/src/main/java/video/api/rtmpdroid/Rtmp.kt: -------------------------------------------------------------------------------- 1 | package video.api.rtmpdroid 2 | 3 | import video.api.rtmpdroid.internal.ExVideoCodecs 4 | import video.api.rtmpdroid.internal.VideoCodecs 5 | import java.io.Closeable 6 | import java.net.ConnectException 7 | import java.net.SocketException 8 | import java.net.SocketTimeoutException 9 | import java.nio.ByteBuffer 10 | 11 | /** 12 | * A RTMP connection. 13 | * 14 | * @param enableWrite if you are broadcasting live stream, set to [Boolean.true]. Otherwise [Boolean.false]. 15 | */ 16 | class Rtmp(private val enableWrite: Boolean = true) : Closeable { 17 | companion object { 18 | init { 19 | RtmpNativeLoader 20 | } 21 | } 22 | 23 | private var ptr: Long 24 | 25 | init { 26 | ptr = nativeAlloc() 27 | if (ptr == 0L) { 28 | throw UnsupportedOperationException("Can't allocate a RTMP context") 29 | } 30 | } 31 | 32 | private external fun nativeIsConnected(): Boolean 33 | 34 | /** 35 | * Check if device it still connected with the remote RTMP server 36 | */ 37 | val isConnected: Boolean 38 | /** 39 | * @return [Boolean.true] if connection is still on. Otherwise [Boolean.false]. 40 | */ 41 | get() = nativeIsConnected() 42 | 43 | private external fun nativeGetTimeout(): Int 44 | private external fun nativeSetTimeout(timeoutInMs: Int): Int 45 | 46 | /** 47 | * Set/get connection timeout in ms 48 | */ 49 | var timeout: Int 50 | /** 51 | * @return connection timeout is ms 52 | */ 53 | get() { 54 | val timeout = nativeGetTimeout() 55 | if (timeout < 0) { 56 | throw UnsupportedOperationException("Can't get timeout") 57 | } 58 | return timeout 59 | } 60 | /** 61 | * @param value connection timeout is ms 62 | */ 63 | set(value) { 64 | if (nativeSetTimeout(value) != 0) { 65 | throw UnsupportedOperationException("Can't set timeout") 66 | } 67 | } 68 | 69 | private external fun nativeGetExVideoCodecs(): String? 70 | private external fun nativeSetExVideoCodec(exVideoCodec: String?): Int 71 | 72 | private external fun nativeGetVideoCodecs(): Int 73 | private external fun nativeSetVideoCodec(videoCodec: Int): Int 74 | 75 | /** 76 | * Set/get supported video codecs. 77 | * It is a list of video mime types. 78 | * 79 | * The supported video codecs will be send in the RTMP `connect` command either in the 80 | * `videoCodecs` for standard codecs or in the `fourCCList` for enhanced codecs. 81 | */ 82 | var supportedVideoCodecs: List 83 | get() { 84 | val videoCodecs = nativeGetVideoCodecs() 85 | if (videoCodecs <= 0) { 86 | throw UnsupportedOperationException("Can't get supported video codecs") 87 | } 88 | val supportedCodecs = VideoCodecs(videoCodecs) 89 | val supportedExCodecs = ExVideoCodecs(nativeGetExVideoCodecs()) 90 | return supportedCodecs.supportedCodecs + supportedExCodecs.supportedCodecs 91 | } 92 | set(value) { 93 | if (value.isEmpty()) { 94 | throw IllegalArgumentException("At least one codec must be supported") 95 | } 96 | 97 | val supportedCodecs = mutableListOf() 98 | val supportedExCodecs = mutableListOf() 99 | value.forEach { 100 | if (VideoCodecs.isSupportedCodec(it)) { 101 | supportedCodecs.add(it) 102 | } else if (ExVideoCodecs.isSupportedCodec(it)) { 103 | supportedExCodecs.add(it) 104 | } else { 105 | throw IllegalArgumentException("Unsupported codec $it") 106 | } 107 | } 108 | 109 | if (nativeSetVideoCodec(VideoCodecs.fromMimeTypes(supportedCodecs).value) != 0) { 110 | throw UnsupportedOperationException("Can't set supported video codecs") 111 | } 112 | 113 | if (nativeSetExVideoCodec(ExVideoCodecs.fromMimeTypes(supportedExCodecs).value) != 0) { 114 | throw UnsupportedOperationException("Can't set supported extended video codecs") 115 | } 116 | } 117 | 118 | private external fun nativeAlloc(): Long 119 | 120 | private external fun nativeSetupURL(url: String): Int 121 | private external fun nativeEnableWrite(): Int 122 | private external fun nativeConnect(): Int 123 | 124 | /** 125 | * Connects to a remote RTMP server. 126 | * 127 | * You must call [connectStream] after. 128 | * To set connect command description, appends name-value pairs to [url]. 129 | * 130 | * @param url valid RTMP url (rtmp://myserver/s/streamKey) 131 | */ 132 | fun connect(url: String) { 133 | if (nativeSetupURL(url) != 0) { 134 | throw IllegalArgumentException("Invalid RTMP URL: $url") 135 | } 136 | 137 | if (enableWrite) { 138 | if (nativeEnableWrite() != 0) { 139 | throw UnsupportedOperationException("Failed to enable write") 140 | } 141 | } 142 | 143 | if (nativeConnect() != 0) { 144 | throw ConnectException("Failed to connect") 145 | } 146 | } 147 | 148 | private external fun nativeConnectStream(): Int 149 | 150 | /** 151 | * Creates a new stream. 152 | * 153 | * @see [deleteStream] 154 | */ 155 | fun connectStream() { 156 | if (nativeConnectStream() != 0) { 157 | throw ConnectException("Failed to connectStream") 158 | } 159 | } 160 | 161 | private external fun nativeDeleteStream(): Int 162 | 163 | /** 164 | * Deletes a running stream. 165 | * 166 | * @see [connectStream] 167 | */ 168 | fun deleteStream() { 169 | if (nativeDeleteStream() != 0) { 170 | throw UnsupportedOperationException("Failed to delete stream") 171 | } 172 | } 173 | 174 | private external fun nativeWrite(buffer: ByteBuffer, offset: Int, size: Int): Int 175 | 176 | /** 177 | * Sends a FLV packet inside a [ByteBuffer]. 178 | * 179 | * @param buffer a direct [ByteBuffer] 180 | * @return number of bytes sent 181 | */ 182 | fun write(buffer: ByteBuffer): Int { 183 | require(buffer.isDirect) { "ByteBuffer must be a direct buffer" } 184 | 185 | val byteSent = synchronized(this) { 186 | nativeWrite(buffer, buffer.position(), buffer.remaining()) 187 | } 188 | when { 189 | byteSent < 0 -> { 190 | throw SocketException("Connection error") 191 | } 192 | 193 | byteSent == 0 -> { 194 | throw SocketTimeoutException("Timeout exception") 195 | } 196 | 197 | else -> return byteSent 198 | } 199 | } 200 | 201 | private external fun nativeWrite( 202 | data: ByteArray, offset: Int, size: Int 203 | ): Int 204 | 205 | /** 206 | * Sends a FLV packet inside a [ByteArray]. 207 | * 208 | * @param array a [ByteArray] 209 | * @return number of bytes sent 210 | */ 211 | fun write(array: ByteArray, offset: Int = 0, size: Int = array.size): Int { 212 | val byteSent = synchronized(this) { 213 | nativeWrite(array, offset, size) 214 | } 215 | when { 216 | byteSent < 0 -> { 217 | throw SocketException("Connection error") 218 | } 219 | 220 | byteSent == 0 -> { 221 | throw SocketTimeoutException("Timeout exception") 222 | } 223 | 224 | else -> return byteSent 225 | } 226 | } 227 | 228 | private external fun nativeRead(data: ByteArray, offset: Int, size: Int): Int 229 | 230 | /** 231 | * Reads FLV packets. 232 | * 233 | * @param array a [ByteArray] where to read incoming data 234 | * @return number of bytes received 235 | */ 236 | fun read(array: ByteArray, offset: Int = 0, size: Int = array.size): Int { 237 | val byteReceived = nativeRead(array, offset, size) 238 | when { 239 | byteReceived < 0 -> { 240 | throw SocketException("Connection error") 241 | } 242 | 243 | byteReceived == 0 -> { 244 | throw SocketTimeoutException("Timeout exception") 245 | } 246 | 247 | else -> return byteReceived 248 | } 249 | } 250 | 251 | private external fun nativeWritePacket(packet: RtmpPacket): Int 252 | 253 | /** 254 | * Write a RTMP packet 255 | * 256 | * @param packet RTMP packet to send 257 | * @see [readPacket] 258 | */ 259 | fun writePacket(packet: RtmpPacket) { 260 | if (nativeWritePacket(packet) != 0) { 261 | throw SocketException("Failed to write packet") 262 | } 263 | } 264 | 265 | private external fun nativeReadPacket(): RtmpPacket? 266 | 267 | /** 268 | * Read a RTMP packet 269 | * 270 | * @return received RTMP packet 271 | * @see [writePacket] 272 | */ 273 | fun readPacket(): RtmpPacket { 274 | val rtmpPacket = nativeReadPacket() 275 | if (rtmpPacket == null) { 276 | throw SocketException("Failed to read packet") 277 | } else { 278 | return rtmpPacket 279 | } 280 | } 281 | 282 | private external fun nativePause(): Int 283 | 284 | /** 285 | * Pauses a stream 286 | * 287 | * @see [resume] 288 | */ 289 | fun pause() { 290 | if (nativePause() != 0) { 291 | throw SocketException("Can't pause") 292 | } 293 | } 294 | 295 | private external fun nativeResume(): Int 296 | 297 | /** 298 | * Resumes a stream after a [pause]. 299 | * 300 | * @see [pause] 301 | */ 302 | fun resume() { 303 | if (nativeResume() != 0) { 304 | throw SocketException("Can't resume") 305 | } 306 | } 307 | 308 | private external fun nativeClose() 309 | 310 | /** 311 | * Closes the RTMP connection. 312 | */ 313 | override fun close() { 314 | if (ptr != 0L) { 315 | nativeClose() 316 | ptr = 0L 317 | } 318 | } 319 | 320 | private external fun nativeServe(fd: Int): Int 321 | 322 | /** 323 | * Handshakes incoming client. 324 | * 325 | * For server only. 326 | * 327 | * @param fd file descriptor of a UNIX socket. 328 | */ 329 | fun serve(fd: Int) { 330 | if (nativeServe(fd) != 0) { 331 | throw SocketException("Can't serve") 332 | } 333 | } 334 | } -------------------------------------------------------------------------------- /lib/src/main/java/video/api/rtmpdroid/RtmpNativeLoader.kt: -------------------------------------------------------------------------------- 1 | package video.api.rtmpdroid 2 | 3 | /** 4 | * Statically loads native library. 5 | * For internal usage only. 6 | */ 7 | object RtmpNativeLoader { 8 | init { 9 | System.loadLibrary("crypto") 10 | System.loadLibrary("ssl") 11 | System.loadLibrary("rtmpdroid") 12 | } 13 | } -------------------------------------------------------------------------------- /lib/src/main/java/video/api/rtmpdroid/RtmpPacket.kt: -------------------------------------------------------------------------------- 1 | package video.api.rtmpdroid 2 | 3 | import java.nio.ByteBuffer 4 | 5 | /** 6 | * RTMP packet. 7 | * Added for test purpose only. 8 | */ 9 | class RtmpPacket( 10 | val channel: Int, 11 | val headerType: Int, 12 | val packetType: Int, 13 | val timestamp: Int, 14 | val buffer: ByteBuffer 15 | ) { 16 | constructor( 17 | channel: Int, 18 | headerType: Int, 19 | packetType: PacketType, 20 | timestamp: Int, 21 | buffer: ByteBuffer 22 | ) : this(channel, headerType, packetType.value, timestamp, buffer) 23 | } 24 | 25 | /** 26 | * RTMP Packet type 27 | * @param value RTMP int equivalent 28 | */ 29 | enum class PacketType(val value: Int) { 30 | 31 | COMMAND(0x14) 32 | } -------------------------------------------------------------------------------- /lib/src/main/java/video/api/rtmpdroid/amf/AmfEncoder.kt: -------------------------------------------------------------------------------- 1 | package video.api.rtmpdroid.amf 2 | 3 | import video.api.rtmpdroid.RtmpNativeLoader 4 | import video.api.rtmpdroid.amf.models.EcmaArray 5 | import video.api.rtmpdroid.amf.models.NamedParameter 6 | import video.api.rtmpdroid.amf.models.NullParameter 7 | import video.api.rtmpdroid.amf.models.ObjectParameter 8 | import java.io.IOException 9 | import java.nio.ByteBuffer 10 | 11 | class AmfEncoder { 12 | private val parameters = mutableListOf() 13 | 14 | /** 15 | * Adds a new parameter. 16 | * Once your are done adding parameters, call [encode]. 17 | * 18 | * @param parameter the new parameter 19 | */ 20 | fun add(parameter: Any) { 21 | parameters.add(parameter) 22 | } 23 | 24 | /** 25 | * Adds a named parameter. 26 | * Same as adding a NamedParameter. 27 | * 28 | * @param name the parameter name 29 | * @param value the parameter value 30 | */ 31 | fun add(name: String, value: Any) { 32 | parameters.add(NamedParameter(name, value)) 33 | } 34 | 35 | /** 36 | * Encodes added parameters. 37 | * 38 | * @return a [ByteBuffer] containing added parameters in AMF 39 | */ 40 | fun encode(): ByteBuffer { 41 | val buffer = ByteBuffer.allocateDirect(minBufferSize) 42 | encode(buffer) 43 | buffer.rewind() 44 | return buffer 45 | } 46 | 47 | /** 48 | * Encodes added parameters. 49 | * 50 | * @param buffer a direct buffer 51 | */ 52 | fun encode(buffer: ByteBuffer) { 53 | require(buffer.isDirect) { "ByteBuffer must be a direct buffer" } 54 | 55 | parameters.forEach { 56 | encode(buffer, it) 57 | } 58 | } 59 | 60 | private fun encode(buffer: ByteBuffer, parameter: Any) { 61 | val size = when (parameter) { 62 | is Boolean -> { 63 | nativeEncodeBoolean(buffer, parameter = parameter) 64 | } 65 | is Int -> { 66 | nativeEncodeInt(buffer, parameter = parameter) 67 | } 68 | is Double -> { 69 | nativeEncodeNumber(buffer, parameter = parameter) 70 | } 71 | is String -> { 72 | nativeEncodeString(buffer, parameter = parameter) 73 | } 74 | is NullParameter -> { 75 | buffer.put(AmfType.NULL.value) 76 | buffer.position() 77 | } 78 | is NamedParameter -> { 79 | when (parameter.value) { 80 | is Boolean -> { 81 | nativeEncodeNamedBoolean( 82 | buffer, 83 | name = parameter.name, 84 | parameter = parameter.value 85 | ) 86 | } 87 | is Double -> { 88 | nativeEncodeNamedNumber( 89 | buffer, 90 | name = parameter.name, 91 | parameter = parameter.value 92 | ) 93 | } 94 | is String -> { 95 | nativeEncodeNamedString( 96 | buffer, 97 | name = parameter.name, 98 | parameter = parameter.value 99 | ) 100 | } 101 | else -> { 102 | throw IOException("Named parameter type is not supported: ${parameter.value::class.java.simpleName}") 103 | } 104 | } 105 | } 106 | is ObjectParameter -> { 107 | buffer.put(AmfType.OBJECT.value) 108 | parameter.parameters.forEach { encode(buffer, it) } 109 | nativeEncodeInt24(buffer, parameter = AmfType.OBJECT_END.value.toInt()) 110 | } 111 | is EcmaArray -> { 112 | buffer.put(AmfType.ECMA_ARRAY.value) 113 | buffer.position(nativeEncodeInt(buffer, parameter = parameter.parameters.size)) 114 | parameter.parameters.forEach { encode(buffer, it) } 115 | nativeEncodeInt24(buffer, parameter = AmfType.OBJECT_END.value.toInt()) 116 | } 117 | else -> throw IOException("Parameter type is not supported: ${parameter::class.java.simpleName}") 118 | } 119 | if (size < 0) { 120 | throw ArrayIndexOutOfBoundsException(buffer.position()) 121 | } 122 | buffer.position(size) 123 | } 124 | 125 | /** 126 | * Get buffer size in bytes. 127 | */ 128 | val minBufferSize: Int 129 | get() = parameters.sumOf { getParameterSize(it) } 130 | 131 | companion object { 132 | init { 133 | RtmpNativeLoader 134 | } 135 | 136 | private fun getParameterSize(parameter: Any): Int { 137 | return when (parameter) { 138 | is Boolean -> { 139 | 2 140 | } 141 | is Short -> { 142 | 2 143 | } 144 | is Int -> { 145 | 4 146 | } 147 | is Double -> { 148 | 9 149 | } 150 | is String -> { 151 | 3 + parameter.length 152 | } 153 | is NullParameter -> { 154 | 1 155 | } 156 | is NamedParameter -> { 157 | 2 /* includes param name size (2 bytes) */ + parameter.name.length + getParameterSize( 158 | parameter.value 159 | ) 160 | } 161 | is ObjectParameter -> { 162 | 4 /* 1 byte for start - 3 bytes for footer */ + parameter.parameters.sumOf { 163 | getParameterSize( 164 | it 165 | ) 166 | } 167 | } 168 | is EcmaArray -> { 169 | 8 /* 1 byte for type + 4 bytes for array size + 3 bytes for footer */ + parameter.parameters.sumOf { 170 | getParameterSize( 171 | it 172 | ) 173 | } 174 | } 175 | else -> throw IOException("Parameter type is not supported: ${parameter::class.java.simpleName}") 176 | } 177 | } 178 | 179 | @JvmStatic 180 | private external fun nativeEncodeBoolean( 181 | buffer: ByteBuffer, 182 | offset: Int = buffer.position(), 183 | end: Int = buffer.limit(), 184 | parameter: Boolean 185 | ): Int 186 | 187 | @JvmStatic 188 | private external fun nativeEncodeInt24( 189 | buffer: ByteBuffer, 190 | offset: Int = buffer.position(), 191 | end: Int = buffer.limit(), 192 | parameter: Int 193 | ): Int 194 | 195 | @JvmStatic 196 | private external fun nativeEncodeInt( 197 | buffer: ByteBuffer, 198 | offset: Int = buffer.position(), 199 | end: Int = buffer.limit(), 200 | parameter: Int 201 | ): Int 202 | 203 | @JvmStatic 204 | private external fun nativeEncodeNumber( 205 | buffer: ByteBuffer, 206 | offset: Int = buffer.position(), 207 | end: Int = buffer.limit(), 208 | parameter: Double 209 | ): Int 210 | 211 | @JvmStatic 212 | private external fun nativeEncodeString( 213 | buffer: ByteBuffer, 214 | offset: Int = buffer.position(), 215 | end: Int = buffer.limit(), 216 | parameter: String 217 | ): Int 218 | 219 | @JvmStatic 220 | private external fun nativeEncodeNamedBoolean( 221 | buffer: ByteBuffer, 222 | offset: Int = buffer.position(), 223 | end: Int = buffer.limit(), 224 | name: String, 225 | parameter: Boolean 226 | ): Int 227 | 228 | @JvmStatic 229 | private external fun nativeEncodeNamedNumber( 230 | buffer: ByteBuffer, 231 | offset: Int = buffer.position(), 232 | end: Int = buffer.limit(), 233 | name: String, 234 | parameter: Double 235 | ): Int 236 | 237 | @JvmStatic 238 | private external fun nativeEncodeNamedString( 239 | buffer: ByteBuffer, 240 | offset: Int = buffer.position(), 241 | end: Int = buffer.limit(), 242 | name: String, 243 | parameter: String 244 | ): Int 245 | } 246 | } 247 | 248 | enum class AmfType(val value: Byte) { 249 | NUMBER(0x00), 250 | BOOLEAN(0x01), 251 | STRING(0x02), 252 | OBJECT(0x03), 253 | NULL(0x05), 254 | ECMA_ARRAY(0x08), 255 | OBJECT_END(0x09) 256 | } -------------------------------------------------------------------------------- /lib/src/main/java/video/api/rtmpdroid/amf/models/EcmaArray.kt: -------------------------------------------------------------------------------- 1 | package video.api.rtmpdroid.amf.models 2 | 3 | /** 4 | * A ECMA array 5 | */ 6 | class EcmaArray { 7 | /** 8 | * List of added parameters 9 | */ 10 | internal val parameters = mutableListOf() 11 | 12 | /** 13 | * Adds a new parameter inside the ECMA array 14 | * 15 | * @param parameter the new parameter 16 | */ 17 | fun add(parameter: Any) { 18 | parameters.add(parameter) 19 | } 20 | 21 | /** 22 | * Adds a named parameter. 23 | * Same as adding a NamedParameter. 24 | * 25 | * @param name the parameter name 26 | * @param value the parameter value 27 | */ 28 | fun add(name: String, value: Any) { 29 | parameters.add(NamedParameter(name, value)) 30 | } 31 | } -------------------------------------------------------------------------------- /lib/src/main/java/video/api/rtmpdroid/amf/models/NamedParameter.kt: -------------------------------------------------------------------------------- 1 | package video.api.rtmpdroid.amf.models 2 | 3 | /** 4 | * Named parameter wrapper 5 | * 6 | * @param name name of the parameter 7 | * @param value value of the parameter 8 | */ 9 | data class NamedParameter(val name: String, val value: Any) -------------------------------------------------------------------------------- /lib/src/main/java/video/api/rtmpdroid/amf/models/NullParameter.kt: -------------------------------------------------------------------------------- 1 | package video.api.rtmpdroid.amf.models 2 | 3 | /** 4 | * Null parameter wrapper 5 | */ 6 | class NullParameter -------------------------------------------------------------------------------- /lib/src/main/java/video/api/rtmpdroid/amf/models/ObjectParameter.kt: -------------------------------------------------------------------------------- 1 | package video.api.rtmpdroid.amf.models 2 | 3 | /** 4 | * A ECMA array 5 | */ 6 | class ObjectParameter { 7 | /** 8 | * List of added parameters 9 | */ 10 | internal val parameters = mutableListOf() 11 | 12 | /** 13 | * Adds a new parameter inside the ECMA array 14 | * 15 | * @param parameter the new parameter 16 | */ 17 | fun add(parameter: Any) { 18 | parameters.add(parameter) 19 | } 20 | 21 | /** 22 | * Adds a named parameter. 23 | * Same as adding a NamedParameter. 24 | * 25 | * @param name the parameter name 26 | * @param value the parameter value 27 | */ 28 | fun add(name: String, value: Any) { 29 | parameters.add(NamedParameter(name, value)) 30 | } 31 | } -------------------------------------------------------------------------------- /lib/src/main/java/video/api/rtmpdroid/internal/VideoCodecs.kt: -------------------------------------------------------------------------------- 1 | package video.api.rtmpdroid.internal 2 | 3 | import android.media.MediaFormat 4 | import android.os.Build 5 | 6 | /** 7 | * Handler for video codecs supported by RTMP protocol. 8 | * It uses to map `videoCodecs` from `connect` command to mime types. 9 | * 10 | * For codecs described in enhanced RTMP, see [ExVideoCodecs] 11 | * 12 | * @param value supported video codecs 13 | */ 14 | class VideoCodecs(val value: Int) { 15 | /** 16 | * The mime type of the codecs contains in the [value] of `videoCodec`. 17 | */ 18 | val supportedCodecs = run { 19 | val list = mutableListOf() 20 | for ((mimeType, codec) in codecsMap) { 21 | if (value and codec != 0) { 22 | list.add(mimeType) 23 | } 24 | } 25 | list 26 | } 27 | 28 | /** 29 | * Whether the value has the codec. 30 | * 31 | * @param mimeType video codec mime type 32 | * @return true if codec is supported, otherwise false 33 | */ 34 | fun hasCodec(mimeType: String): Boolean { 35 | return supportedCodecs.contains(mimeType) 36 | } 37 | 38 | companion object { 39 | private const val SUPPORT_VID_UNUSED = 0x0001 40 | private const val SUPPORT_VID_JPEG = 0x0002 41 | private const val SUPPORT_VID_SORENSON = 0x0004 42 | private const val SUPPORT_VID_HOMEBREW = 0x0008 43 | private const val SUPPORT_VID_VP6 = 0x0010 44 | private const val SUPPORT_VID_VP6ALPHA = 0x0020 45 | private const val SUPPORT_VID_HOMEBREWV = 0x0040 46 | private const val SUPPORT_VID_H264 = 0x0080 47 | 48 | private val codecsMap = mapOf( 49 | MediaFormat.MIMETYPE_VIDEO_H263 to SUPPORT_VID_SORENSON, 50 | MediaFormat.MIMETYPE_VIDEO_AVC to SUPPORT_VID_H264 51 | ) 52 | 53 | /** 54 | * Whether the mime type is a RTMP supported codec. 55 | * 56 | * @param mimeType video codec mime type 57 | * @return true if codec is supported by RTMP, otherwise false 58 | */ 59 | fun isSupportedCodec(mimeType: String): Boolean { 60 | return codecsMap.containsKey(mimeType) 61 | } 62 | 63 | /** 64 | * Creates a [VideoCodecs] from a list of mime types. 65 | * 66 | * @param mimeTypes list of mime types 67 | * @return [VideoCodecs] instance 68 | */ 69 | fun fromMimeTypes(mimeTypes: List): VideoCodecs { 70 | var value = 0 71 | for (mimeType in mimeTypes) { 72 | val codec = codecsMap[mimeType] 73 | if (codec != null) { 74 | value = value or codec 75 | } else { 76 | throw IllegalArgumentException("Mimetype $mimeType is not supported by RTMP protocol") 77 | } 78 | } 79 | return VideoCodecs(value) 80 | } 81 | } 82 | } 83 | 84 | /** 85 | * Handler for video codecs supported by enhanced RTMP protocol. 86 | * 87 | * @param value supported video codecs with format `hvc1[,av01][,vp09]` 88 | */ 89 | class ExVideoCodecs(val value: String?) { 90 | private val fourCCList = value?.split(",")?.toList() ?: emptyList() 91 | 92 | init { 93 | for (mimeType in supportedCodecs) { 94 | if (!isSupportedCodec(mimeType)) { 95 | throw IllegalArgumentException("Mimetype $mimeType is not supported by enhanced RTMP protocol") 96 | } 97 | } 98 | } 99 | 100 | /** 101 | * The mime type of the codecs contains in the [value] of `fourCcList`. 102 | */ 103 | val supportedCodecs: List 104 | get() { 105 | val list = mutableListOf() 106 | for ((mimeType, fourCC) in codecsMap) { 107 | if (fourCCList.contains(fourCC)) { 108 | list.add(mimeType) 109 | } 110 | } 111 | return list 112 | } 113 | 114 | /** 115 | * Whether the value has the codec. 116 | * 117 | * @param mimeType video codec mime type 118 | * @return true if codec is supported, otherwise false 119 | */ 120 | fun hasCodec(mimeType: String): Boolean { 121 | return supportedCodecs.contains(mimeType) 122 | } 123 | 124 | companion object { 125 | private const val AV1_FOURCC_TAG = "av01" 126 | private const val VP9_FOURCC_TAG = "vp9" 127 | private const val HEVC_FOURCC_TAG = "hvc1" 128 | 129 | private val codecsMap = mutableMapOf( 130 | MediaFormat.MIMETYPE_VIDEO_VP9 to VP9_FOURCC_TAG, 131 | MediaFormat.MIMETYPE_VIDEO_HEVC to HEVC_FOURCC_TAG 132 | ).apply { 133 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 134 | this[MediaFormat.MIMETYPE_VIDEO_AV1] = AV1_FOURCC_TAG 135 | } 136 | }.toMap() 137 | 138 | /** 139 | * Whether the mime type is a RTMP supported extended codec. 140 | * 141 | * @param mimeType video codec mime type 142 | * @return true if codec is supported by enhanced RTMP, otherwise false 143 | */ 144 | fun isSupportedCodec(mimeType: String): Boolean { 145 | return codecsMap.containsKey(mimeType) 146 | } 147 | 148 | /** 149 | * Creates a [ExVideoCodecs] from a list of mime types. 150 | */ 151 | fun fromMimeTypes(mimeTypes: List): ExVideoCodecs { 152 | if (mimeTypes.isEmpty()) { 153 | return ExVideoCodecs(null) 154 | } 155 | val value = mutableListOf() 156 | for (mimeType in mimeTypes) { 157 | val codec = codecsMap[mimeType] 158 | if (codec != null) { 159 | value.add(codec) 160 | } else { 161 | throw IllegalArgumentException("Mimetype $mimeType is not supported by enhanced RTMP protocol") 162 | } 163 | } 164 | return ExVideoCodecs(value.joinToString(",")) 165 | } 166 | } 167 | } -------------------------------------------------------------------------------- /lib/src/test/java/video/api/rtmpdroid/internal/ExVideoCodecsTest.kt: -------------------------------------------------------------------------------- 1 | package video.api.rtmpdroid.internal 2 | 3 | import android.media.MediaFormat 4 | import org.junit.Assert 5 | import org.junit.Test 6 | 7 | class ExVideoCodecsTest { 8 | @Test 9 | fun `test fromMimeTypes`() { 10 | val list = listOf(MediaFormat.MIMETYPE_VIDEO_HEVC) 11 | val videoCodecs = ExVideoCodecs.fromMimeTypes(list) 12 | Assert.assertTrue(videoCodecs.supportedCodecs.contains(MediaFormat.MIMETYPE_VIDEO_HEVC)) 13 | } 14 | 15 | @Test 16 | fun `test fromMimeTypes with invalid codec`() { 17 | val list = listOf(MediaFormat.MIMETYPE_VIDEO_H263, MediaFormat.MIMETYPE_VIDEO_AV1) 18 | try { 19 | ExVideoCodecs.fromMimeTypes(list) 20 | Assert.fail("IllegalArgumentException should be thrown for H263 codec") 21 | } catch (_: IllegalArgumentException) { 22 | } 23 | } 24 | 25 | @Test 26 | fun `test empty fromMimeTypes`() { 27 | val list = emptyList() 28 | val videoCodecs = ExVideoCodecs.fromMimeTypes(list) 29 | Assert.assertNull(videoCodecs.value) 30 | Assert.assertTrue(videoCodecs.supportedCodecs.isEmpty()) 31 | } 32 | } -------------------------------------------------------------------------------- /lib/src/test/java/video/api/rtmpdroid/internal/VideoCodecsTest.kt: -------------------------------------------------------------------------------- 1 | package video.api.rtmpdroid.internal 2 | 3 | import android.media.MediaFormat 4 | import org.junit.Assert.assertTrue 5 | import org.junit.Assert.fail 6 | import org.junit.Test 7 | 8 | class VideoCodecsTest { 9 | @Test 10 | fun `test fromMimeTypes`() { 11 | val list = listOf(MediaFormat.MIMETYPE_VIDEO_H263) 12 | val videoCodecs = VideoCodecs.fromMimeTypes(list) 13 | assertTrue(videoCodecs.value == 0x4) 14 | assertTrue(videoCodecs.supportedCodecs.contains(MediaFormat.MIMETYPE_VIDEO_H263)) 15 | } 16 | 17 | @Test 18 | fun `test fromMimeTypes with invalid codec`() { 19 | val list = listOf(MediaFormat.MIMETYPE_VIDEO_H263, MediaFormat.MIMETYPE_VIDEO_AV1) 20 | try { 21 | VideoCodecs.fromMimeTypes(list) 22 | fail("IllegalArgumentException should be thrown for AV1 codec") 23 | } catch (_: IllegalArgumentException) { 24 | } 25 | } 26 | 27 | @Test 28 | fun `test empty fromMimeTypes`() { 29 | val list = emptyList() 30 | val videoCodecs = VideoCodecs.fromMimeTypes(list) 31 | assertTrue(videoCodecs.value == 0) 32 | assertTrue(videoCodecs.supportedCodecs.isEmpty()) 33 | } 34 | } -------------------------------------------------------------------------------- /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 | 16 | rootProject.name = "rtmpdroid" 17 | include ':app' 18 | include ':lib' 19 | --------------------------------------------------------------------------------