├── .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 | [](https://twitter.com/intent/follow?screen_name=api_video)
2 | [](https://github.com/apivideo/api.video-rtmpdroid)
3 | [](https://community.api.video)
4 | 
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 |
--------------------------------------------------------------------------------
/docs/logo-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------