Showing preview only (379K chars total). Download the full file or copy to clipboard to get everything.
Repository: alexeyvasilyev/rtsp-client-android
Branch: master
Commit: 0c0dd61e20eb
Files: 66
Total size: 354.9 KB
Directory structure:
gitextract_xa9itofy/
├── .github/
│ └── FUNDING.yml
├── .gitignore
├── LICENSE
├── README.md
├── app/
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── java/
│ │ └── com/
│ │ └── alexvas/
│ │ └── rtsp/
│ │ └── demo/
│ │ ├── MainActivity.kt
│ │ └── live/
│ │ ├── LiveFragment.kt
│ │ ├── LiveViewModel.kt
│ │ └── RawFragment.kt
│ └── res/
│ ├── drawable/
│ │ ├── ic_camera_black_24dp.xml
│ │ ├── ic_cctv_black_24dp.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── ic_launcher_foreground.xml
│ │ └── ic_text_subject_black_24dp.xml
│ ├── layout/
│ │ ├── activity_main.xml
│ │ ├── fragment_live.xml
│ │ ├── fragment_logs.xml
│ │ ├── fragment_raw.xml
│ │ └── layout_rtsp_params.xml
│ ├── menu/
│ │ └── bottom_nav_menu.xml
│ ├── mipmap-anydpi-v26/
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ ├── navigation/
│ │ └── mobile_navigation.xml
│ └── values/
│ ├── colors.xml
│ ├── dimens.xml
│ ├── strings.xml
│ └── styles.xml
├── build.gradle
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── jitpack.yml
├── library-client-rtsp/
│ ├── .gitignore
│ ├── build.gradle
│ ├── proguard-rules.txt
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ └── java/
│ └── com/
│ ├── alexvas/
│ │ ├── rtsp/
│ │ │ ├── RtspClient.java
│ │ │ ├── codec/
│ │ │ │ ├── AudioDecodeThread.kt
│ │ │ │ ├── FrameQueue.kt
│ │ │ │ ├── VideoDecodeThread.kt
│ │ │ │ ├── VideoDecoderBitmapThread.kt
│ │ │ │ ├── VideoDecoderSurfaceThread.kt
│ │ │ │ └── color/
│ │ │ │ ├── ColorConverter.kt
│ │ │ │ └── ColorConverterImage.kt
│ │ │ ├── parser/
│ │ │ │ ├── AacParser.java
│ │ │ │ ├── AudioParser.kt
│ │ │ │ ├── G711Parser.kt
│ │ │ │ ├── RtpH264Parser.kt
│ │ │ │ ├── RtpH265Parser.kt
│ │ │ │ ├── RtpHeaderParser.java
│ │ │ │ └── RtpParser.kt
│ │ │ └── widget/
│ │ │ ├── RtspImageView.kt
│ │ │ ├── RtspListeners.kt
│ │ │ ├── RtspProcessor.kt
│ │ │ └── RtspSurfaceView.kt
│ │ └── utils/
│ │ ├── ByteUtils.java
│ │ ├── MediaCodecUtils.kt
│ │ ├── NetUtils.java
│ │ └── VideoCodecUtils.kt
│ └── limelight/
│ └── binding/
│ └── video/
│ └── MediaCodecHelper.java
└── settings.gradle
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
github: alexeyvasilyev
================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea
/build
.DS_Store
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# rtsp-client-android
<b>Lightweight RTSP client library for Android</b> with almost zero lag video decoding (achieved 20 msec video decoding latency on some RTSP streams). Designed for lag criticial applications (e.g. video surveillance from drones, car rear view cameras, etc.).
Unlike [AndroidX Media ExoPlayer](https://github.com/androidx/media) which also supports RTSP, this library does not make any video buffering. Video frames are shown immidiately when they arrive.
[](https://jitpack.io/#alexeyvasilyev/rtsp-client-android)

## Features:
- RTSP/RTSPS over TCP.
- Supports majority of RTSP IP cameras.
- Video H.264/H.265.
- Audio AAC LC, G.711 uLaw, G.711 aLaw.
- Support for application specific data sent via RTP, e.g. GPS data (`m=application`, see [RFC 4566 sec.5.14](https://datatracker.ietf.org/doc/html/rfc4566#section-5.14))
- Basic/Digest authentication.
- Uses Android's [Low-Latency MediaCodec](https://source.android.com/docs/core/media/low-latency-media) by default if available.
- Ability to select hardware or software video decoder.
- Ability to [rewrite SPS frame](https://github.com/alexeyvasilyev/rtsp-client-android/blob/dbea741548307b1b0e1ead0ccc6294e811fbf6fd/library-client-rtsp/src/main/java/com/alexvas/rtsp/widget/RtspProcessor.kt#L106C9-L106C55) with low-latency parameters (EXPERIMENTAL).
- Video rotation (90, 180, 270 degrees).
- Android min API 24.
## Upcoming features:
- 2-w talk.
## Permissions:
```xml
<uses-permission android:name="android.permission.INTERNET" />
```
## Compile
To use this library in your project add this to your build.gradle:
```gradle
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
dependencies {
implementation 'com.github.alexeyvasilyev:rtsp-client-android:x.x.x'
}
```
## How to use:
Easiest way is just to use `RtspSurfaceView` (recommended) or `RtspImageView` classes for showing video stream in UI.
Use [RtspSurfaceView](https://github.com/alexeyvasilyev/rtsp-client-android/blob/master/library-client-rtsp/src/main/java/com/alexvas/rtsp/widget/RtspSurfaceView.kt) if you need best performance and less battery usage. To get bitmap from SurfaceView use [PixelCopy.request](https://developer.android.com/reference/android/view/PixelCopy) (on Pixel 8 Pro with 1440p @ 20 fps video stream, you can get 12 fps only via PixelCopy)
Use [RtspImageView](https://github.com/alexeyvasilyev/rtsp-client-android/blob/master/library-client-rtsp/src/main/java/com/alexvas/rtsp/widget/RtspImageView.kt) if you need better performance than PixelCopy for getting bitmaps for further processing (e.g. for AI).
```xml
<com.alexvas.rtsp.widget.RtspSurfaceView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/svVideo" />
<com.alexvas.rtsp.widget.RtspImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/ivVideo" />
```
Then in code use:
```kotlin
val uri = Uri.parse("rtsps://10.0.1.3/test.sdp")
val username = "admin"
val password = "secret"
svVideo.init(uri, username, password)
svVideo.start(
requestVideo = true,
requestAudio = true,
requestApplication = false)
// ...
svVideo.stop()
```
You can still use library without any decoding (just for obtaining raw frames from RTSP source), e.g. for writing video stream into MP4 via muxer.
```kotlin
val rtspClientListener = object: RtspClient.RtspClientListener {
override fun onRtspConnecting() {}
override fun onRtspConnected(sdpInfo: SdpInfo) {}
override fun onRtspVideoNalUnitReceived(data: ByteArray, offset: Int, length: Int, timestamp: Long) {
// Send raw H264/H265 NAL unit to decoder
}
override fun onRtspAudioSampleReceived(data: ByteArray, offset: Int, length: Int, timestamp: Long) {
// Send raw audio to decoder
}
override fun onRtspApplicationDataReceived(data: ByteArray, offset: Int, length: Int, timestamp: Long) {
// Send raw application data to app specific parser
}
override fun onRtspDisconnected() {}
override fun onRtspFailedUnauthorized() {
Log.e(TAG, "RTSP failed unauthorized");
}
override fun onRtspFailed(message: String?) {
Log.e(TAG, "RTSP failed with message '$message'")
}
}
val uri = Uri.parse("rtsps://10.0.1.3/test.sdp")
val username = "admin"
val password = "secret"
val stopped = new AtomicBoolean(false)
val sslSocket = NetUtils.createSslSocketAndConnect(uri.getHost(), uri.getPort(), 5000)
val rtspClient = RtspClient.Builder(sslSocket, uri.toString(), stopped, rtspClientListener)
.requestVideo(true)
.requestAudio(true)
.withDebug(false)
.withUserAgent("RTSP client")
.withCredentials(username, password)
.build()
// Blocking call until stopped variable is true or connection failed
rtspClient.execute()
NetUtils.closeSocket(sslSocket)
```
## How to get lowest possible latency:
There are two types of latencies:
### Network latency
If you want the lowest possible network latency, be sure that both Android device and RTSP camera are connected to the same network by the Ethernet cable (not WiFi).
Another option to try is to decrease stream bitrate on RTSP camera. Less frame size leads to less time needed for frame transfer.
### Video decoder latency
Video decoder latency can vary significantly on different Android devices and on different RTSP camera streams.
For the same profile/level and resolution (but different cameras) the latency in best cases can can be 20 msec, in worst cases 1200 msec.
To decrease latency be sure you use the lowest possible H.264 video stream profile and level (enable `debug` in the library and check SPS frame params `profile_idc` and `level_idc` in the log). `Baseline profile` should have the lowest possible decoder latency.
Check `max_num_reorder_frames` param as well. For best latency it's value should be `0`.
You can also try to use [experimentalUpdateSpsFrameWithLowLatencyParams](https://github.com/alexeyvasilyev/rtsp-client-android/blob/master/library-client-rtsp/src/main/java/com/alexvas/rtsp/widget/RtspProcessor.kt#L106) library feature which rewrites config frame on runtime with low-latency parameters.
================================================
FILE: app/.gitignore
================================================
# Created by https://www.gitignore.io/api/android,java,intellij
### Android ###
# Built application files
*.apk
*.ap_
# Files for the Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
xactmobile/class_files.txt
xactmobile/mapping.txt
xactmobile/seeds.txt
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
### Android Patch ###
gen-external-apklibs
### Java ###
*.class
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
#*.jar
*.war
*.ear
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
*.iml
## Directory-based project format:
.idea/
# if you remove the above rule, at least ignore the following:
# User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/dataSources.ids
.idea/dataSources.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## File-based project format:
*.ipr
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
xactmobile/.DS_Store~64be78fe3602626c61b52bcbfd09e09a6107b50a
xactmobile/.DS_Store~HEAD
oslab-viewpager/._.DS_Store
oslab-viewpager/src/main/.DS_Store
oslab-viewpager/src/main/._.DS_Store
oslab-viewpager/src/main/res/.DS_Store
oslab-viewpager/src/main/res/._.DS_Store
oslab-viewpager/.gitignore
oslab-materialdesign/.DS_Store
oslab-materialdesign/._.DS_Store
oslab-materialdesign/src/.DS_Store
oslab-materialdesign/src/._.DS_Store
oslab-materialdesign/src/main/.DS_Store
oslab-materialdesign/src/main/._.DS_Store
oslab-materialdesign/src/main/res/.DS_Store
oslab-materialdesign/src/main/res/._.DS_Store
================================================
FILE: app/build.gradle
================================================
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 36
defaultConfig {
applicationId "com.alexvas.rtsp.demo"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
// To inline the bytecode built with JVM target 1.8 into
// bytecode that is being built with JVM target 1.6. (e.g. navArgs)
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
buildFeatures {
viewBinding true
}
namespace 'com.alexvas.rtsp.demo'
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'androidx.core:core-ktx:1.18.0'
implementation 'com.google.android.material:material:1.13.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
def androidx_navigation_version = '2.9.7'
implementation "androidx.navigation:navigation-fragment-ktx:$androidx_navigation_version"
implementation "androidx.navigation:navigation-ui-ktx:$androidx_navigation_version"
implementation "androidx.navigation:navigation-fragment-ktx:$androidx_navigation_version"
implementation "androidx.navigation:navigation-ui-ktx:$androidx_navigation_version"
def logcat_core_version = '3.4'
api "com.github.AppDevNext.Logcat:LogcatCoreLib:$logcat_core_version"
api "com.github.AppDevNext.Logcat:LogcatCoreUI:$logcat_core_version"
implementation project(':library-client-rtsp')
}
================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
================================================
FILE: app/src/main/java/com/alexvas/rtsp/demo/MainActivity.kt
================================================
package com.alexvas.rtsp.demo
import android.os.Bundle
import com.google.android.material.bottomnavigation.BottomNavigationView
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.findNavController
import androidx.navigation.ui.setupWithNavController
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navView: BottomNavigationView = findViewById(R.id.nav_view)
val navController = findNavController(R.id.nav_host_fragment)
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
// val appBarConfiguration = AppBarConfiguration(setOf(
// R.id.navigation_live, R.id.navigation_logs))
// setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
}
}
================================================
FILE: app/src/main/java/com/alexvas/rtsp/demo/live/LiveFragment.kt
================================================
package com.alexvas.rtsp.demo.live
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
import android.util.Log
import android.view.LayoutInflater
import android.view.PixelCopy
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.net.toUri
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.alexvas.rtsp.codec.VideoDecodeThread
import com.alexvas.rtsp.demo.databinding.FragmentLiveBinding
import com.alexvas.rtsp.widget.RtspDataListener
import com.alexvas.rtsp.widget.RtspImageView
import com.alexvas.rtsp.widget.RtspStatusListener
import com.alexvas.rtsp.widget.toHexString
import java.util.Timer
import java.util.TimerTask
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.min
@SuppressLint("LogNotTimber")
class LiveFragment : Fragment() {
private lateinit var binding: FragmentLiveBinding
private lateinit var liveViewModel: LiveViewModel
private var statisticsTimer: Timer? = null
private var svVideoSurfaceResolution = Pair(0, 0)
private val rtspStatusSurfaceListener = object: RtspStatusListener {
override fun onRtspStatusConnecting() {
if (DEBUG) Log.v(TAG, "onRtspStatusConnecting()")
binding.apply {
tvStatusSurface.text = "RTSP connecting"
pbLoadingSurface.visibility = View.VISIBLE
vShutterSurface.visibility = View.VISIBLE
llRtspParams.apply {
etRtspRequest.isEnabled = false
etRtspUsername.isEnabled = false
etRtspPassword.isEnabled = false
cbVideo.isEnabled = false
cbAudio.isEnabled = false
cbApplication.isEnabled = false
cbDebug.isEnabled = false
}
tgRotation.isEnabled = false
}
}
override fun onRtspStatusConnected() {
if (DEBUG) Log.v(TAG, "onRtspStatusConnected()")
binding.apply {
tvStatusSurface.text = "RTSP connected"
bnStartStopSurface.text = "Stop RTSP"
}
setKeepScreenOn(true)
}
override fun onRtspStatusDisconnecting() {
if (DEBUG) Log.v(TAG, "onRtspStatusDisconnecting()")
binding.apply {
tvStatusSurface.text = "RTSP disconnecting"
}
}
override fun onRtspStatusDisconnected() {
if (DEBUG) Log.v(TAG, "onRtspStatusDisconnected()")
binding.apply {
tvStatusSurface.text = "RTSP disconnected"
bnStartStopSurface.text = "Start RTSP"
pbLoadingSurface.visibility = View.GONE
vShutterSurface.visibility = View.VISIBLE
pbLoadingSurface.isEnabled = false
llRtspParams.apply {
cbVideo.isEnabled = true
cbAudio.isEnabled = true
cbApplication.isEnabled = true
cbDebug.isEnabled = true
etRtspRequest.isEnabled = true
etRtspUsername.isEnabled = true
etRtspPassword.isEnabled = true
}
tgRotation.isEnabled = true
}
setKeepScreenOn(false)
}
override fun onRtspStatusFailedUnauthorized() {
if (DEBUG) Log.e(TAG, "onRtspStatusFailedUnauthorized()")
if (context == null) return
onRtspStatusDisconnected()
binding.apply {
tvStatusSurface.text = "RTSP username or password invalid"
pbLoadingSurface.visibility = View.GONE
}
}
override fun onRtspStatusFailed(message: String?) {
if (DEBUG) Log.e(TAG, "onRtspStatusFailed(message='$message')")
if (context == null) return
onRtspStatusDisconnected()
binding.apply {
tvStatusSurface.text = "Error: $message"
pbLoadingSurface.visibility = View.GONE
}
}
override fun onRtspFirstFrameRendered() {
if (DEBUG) Log.v(TAG, "onRtspFirstFrameRendered()")
Log.i(TAG, "First frame rendered")
binding.apply {
pbLoadingSurface.visibility = View.GONE
vShutterSurface.visibility = View.GONE
bnSnapshotSurface.isEnabled = true
}
}
override fun onRtspFrameSizeChanged(width: Int, height: Int) {
if (DEBUG) Log.v(TAG, "onRtspFrameSizeChanged(width=$width, height=$height)")
Log.i(TAG, "Video resolution changed to ${width}x${height}")
svVideoSurfaceResolution = Pair(width, height)
ConstraintSet().apply {
clone(binding.csVideoSurface)
setDimensionRatio(binding.svVideoSurface.id, "$width:$height")
applyTo(binding.csVideoSurface)
}
}
}
private val rtspDataListener = object: RtspDataListener {
override fun onRtspDataApplicationDataReceived(data: ByteArray, offset: Int, length: Int, timestamp: Long) {
val numBytesDump = min(length, 25) // dump max 25 bytes
Log.i(TAG, "RTSP app data ($length bytes): ${data.toHexString(offset, offset + numBytesDump)}")
}
}
private val rtspStatusImageListener = object: RtspStatusListener {
override fun onRtspStatusConnecting() {
if (DEBUG) Log.v(TAG, "onRtspStatusConnecting()")
binding.apply {
tvStatusImage.text = "RTSP connecting"
pbLoadingImage.visibility = View.VISIBLE
vShutterImage.visibility = View.VISIBLE
}
}
override fun onRtspStatusConnected() {
if (DEBUG) Log.v(TAG, "onRtspStatusConnected()")
binding.apply {
tvStatusImage.text = "RTSP connected"
bnStartStopImage.text = "Stop RTSP"
}
setKeepScreenOn(true)
}
override fun onRtspStatusDisconnecting() {
if (DEBUG) Log.v(TAG, "onRtspStatusDisconnecting()")
binding.apply {
tvStatusImage.text = "RTSP disconnecting"
}
}
override fun onRtspStatusDisconnected() {
if (DEBUG) Log.v(TAG, "onRtspStatusDisconnected()")
binding.apply {
tvStatusImage.text = "RTSP disconnected"
bnStartStopImage.text = "Start RTSP"
pbLoadingImage.visibility = View.GONE
vShutterImage.visibility = View.VISIBLE
pbLoadingImage.isEnabled = false
}
setKeepScreenOn(false)
}
override fun onRtspStatusFailedUnauthorized() {
if (DEBUG) Log.e(TAG, "onRtspStatusFailedUnauthorized()")
if (context == null) return
onRtspStatusDisconnected()
binding.apply {
tvStatusImage.text = "RTSP username or password invalid"
pbLoadingImage.visibility = View.GONE
}
}
override fun onRtspStatusFailed(message: String?) {
if (DEBUG) Log.e(TAG, "onRtspStatusFailed(message='$message')")
if (context == null) return
onRtspStatusDisconnected()
binding.apply {
tvStatusImage.text = "Error: $message"
pbLoadingImage.visibility = View.GONE
}
}
override fun onRtspFirstFrameRendered() {
if (DEBUG) Log.v(TAG, "onRtspFirstFrameRendered()")
Log.i(TAG, "First frame rendered")
binding.apply {
vShutterImage.visibility = View.GONE
pbLoadingImage.visibility = View.GONE
}
}
override fun onRtspFrameSizeChanged(width: Int, height: Int) {
if (DEBUG) Log.v(TAG, "onRtspFrameSizeChanged(width=$width, height=$height)")
Log.i(TAG, "Video resolution changed to ${width}x${height}")
ConstraintSet().apply {
clone(binding.csVideoImage)
setDimensionRatio(binding.ivVideoImage.id, "$width:$height")
applyTo(binding.csVideoImage)
}
}
}
private fun getSnapshot(): Bitmap? {
if (DEBUG) Log.v(TAG, "getSnapshot()")
val surfaceBitmap = Bitmap.createBitmap(
svVideoSurfaceResolution.first,
svVideoSurfaceResolution.second,
Bitmap.Config.ARGB_8888
)
val lock = Object()
val success = AtomicBoolean(false)
val thread = HandlerThread("PixelCopyHelper")
thread.start()
val sHandler = Handler(thread.looper)
val listener = PixelCopy.OnPixelCopyFinishedListener { copyResult ->
success.set(copyResult == PixelCopy.SUCCESS)
synchronized (lock) {
lock.notify()
}
}
synchronized (lock) {
PixelCopy.request(binding.svVideoSurface.holder.surface, surfaceBitmap, listener, sHandler)
lock.wait()
}
thread.quitSafely()
return if (success.get()) surfaceBitmap else null
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
if (DEBUG) Log.v(TAG, "onCreateView()")
liveViewModel = ViewModelProvider(this)[LiveViewModel::class.java]
binding = FragmentLiveBinding.inflate(inflater, container, false)
binding.bnVideoDecoderGroup.check(binding.bnVideoDecoderHardware.id)
binding.svVideoSurface.setStatusListener(rtspStatusSurfaceListener)
binding.svVideoSurface.setDataListener(rtspDataListener)
binding.ivVideoImage.setStatusListener(rtspStatusImageListener)
binding.ivVideoImage.setDataListener(rtspDataListener)
liveViewModel.initEditTexts(
binding.llRtspParams.etRtspRequest,
binding.llRtspParams.etRtspUsername,
binding.llRtspParams.etRtspPassword
)
liveViewModel.rtspRequest.observe(viewLifecycleOwner) {
if (binding.llRtspParams.etRtspRequest.text.toString() != it)
binding.llRtspParams.etRtspRequest.setText(it)
}
liveViewModel.rtspUsername.observe(viewLifecycleOwner) {
if (binding.llRtspParams.etRtspUsername.text.toString() != it)
binding.llRtspParams.etRtspUsername.setText(it)
}
liveViewModel.rtspPassword.observe(viewLifecycleOwner) {
if (binding.llRtspParams.etRtspPassword.text.toString() != it)
binding.llRtspParams.etRtspPassword.setText(it)
}
binding.cbVideoFpsStabilization.setOnCheckedChangeListener { _, isChecked ->
binding.svVideoSurface.videoFrameRateStabilization = isChecked
}
binding.cbExperimentalRewriteSps.setOnCheckedChangeListener { _, isChecked ->
binding.svVideoSurface.experimentalUpdateSpsFrameWithLowLatencyParams = isChecked
}
binding.bnRotate0.setOnClickListener {
binding.svVideoSurface.videoRotation = 0
binding.ivVideoImage.videoRotation = 0
}
binding.bnRotate90.setOnClickListener {
binding.svVideoSurface.videoRotation = 90
binding.ivVideoImage.videoRotation = 90
}
binding.bnRotate180.setOnClickListener {
binding.svVideoSurface.videoRotation = 180
binding.ivVideoImage.videoRotation = 180
}
binding.bnRotate270.setOnClickListener {
binding.svVideoSurface.videoRotation = 270
binding.ivVideoImage.videoRotation = 270
}
binding.bnRotate0.performClick()
binding.bnVideoDecoderHardware.setOnClickListener {
binding.svVideoSurface.videoDecoderType = VideoDecodeThread.DecoderType.HARDWARE
binding.ivVideoImage.videoDecoderType = VideoDecodeThread.DecoderType.HARDWARE
}
binding.bnVideoDecoderSoftware.setOnClickListener {
binding.svVideoSurface.videoDecoderType = VideoDecodeThread.DecoderType.SOFTWARE
binding.ivVideoImage.videoDecoderType = VideoDecodeThread.DecoderType.SOFTWARE
}
binding.bnStartStopSurface.setOnClickListener {
if (binding.svVideoSurface.isStarted()) {
binding.svVideoSurface.stop()
stopStatistics()
} else {
val uri = liveViewModel.rtspRequest.value!!.toUri()
binding.svVideoSurface.apply {
init(
uri,
username = liveViewModel.rtspUsername.value,
password = liveViewModel.rtspPassword.value,
userAgent = "rtsp-client-android"
)
debug = binding.llRtspParams.cbDebug.isChecked
videoFrameRateStabilization = binding.cbVideoFpsStabilization.isChecked
start(
requestVideo = binding.llRtspParams.cbVideo.isChecked,
requestAudio = binding.llRtspParams.cbAudio.isChecked,
requestApplication = binding.llRtspParams.cbApplication.isChecked
)
}
startStatistics()
}
}
binding.bnStartStopImage.setOnClickListener {
if (binding.ivVideoImage.isStarted()) {
binding.ivVideoImage.stop()
stopStatistics()
} else {
val uri = liveViewModel.rtspRequest.value!!.toUri()
binding.ivVideoImage.apply {
init(
uri,
username = liveViewModel.rtspUsername.value,
password = liveViewModel.rtspPassword.value,
userAgent = "rtsp-client-android"
)
debug = binding.llRtspParams.cbDebug.isChecked
onRtspImageBitmapListener = object : RtspImageView.RtspImageBitmapListener {
override fun onRtspImageBitmapObtained(bitmap: Bitmap) {
// TODO: You can send bitmap for processing
}
}
start(
requestVideo = binding.llRtspParams.cbVideo.isChecked,
requestAudio = binding.llRtspParams.cbAudio.isChecked,
requestApplication = binding.llRtspParams.cbApplication.isChecked
)
}
startStatistics()
}
}
binding.bnSnapshotSurface.setOnClickListener {
val bitmap = getSnapshot()
// TODO Save snapshot to DCIM folder
if (bitmap != null) {
Toast.makeText(requireContext(), "Snapshot succeeded ${bitmap.width}x${bitmap.height}", Toast.LENGTH_LONG).show()
} else {
Toast.makeText(requireContext(), "Snapshot failed", Toast.LENGTH_LONG).show()
}
}
return binding.root
}
override fun onResume() {
if (DEBUG) Log.v(TAG, "onResume()")
super.onResume()
liveViewModel.loadParams(requireContext())
}
override fun onPause() {
val started = binding.svVideoSurface.isStarted()
if (DEBUG) Log.v(TAG, "onPause(), started:$started")
super.onPause()
liveViewModel.saveParams(requireContext())
if (started) {
binding.svVideoSurface.stop()
stopStatistics()
}
}
private fun startStatistics() {
if (DEBUG) Log.v(TAG, "startStatistics()")
Log.i(TAG, "Start statistics")
if (statisticsTimer == null) {
val task: TimerTask = object : TimerTask() {
override fun run() {
val statistics = binding.svVideoSurface.statistics
val text =
"Video decoder: ${statistics.videoDecoderType.toString().lowercase()} ${if (statistics.videoDecoderName.isNullOrEmpty()) "" else "(${statistics.videoDecoderName})"}" +
"\nVideo decoder latency: ${statistics.videoDecoderLatencyMsec} ms" +
"\nResolution: ${svVideoSurfaceResolution.first}x${svVideoSurfaceResolution.second}"
// "\nNetwork latency: "
// // Assume that difference between current Android time and camera time cannot be more than 5 sec.
// // Otherwise time need to be synchronized on both devices.
// text += if (statistics.networkLatencyMsec == -1) {
// "-"
// } else if (statistics.networkLatencyMsec < 0 || statistics.networkLatencyMsec > TimeUnit.SECONDS.toMillis(5)) {
// "[time out of sync]"
// } else {
// "${statistics.networkLatencyMsec} ms"
// }
binding.tvStatistics.post {
binding.tvStatistics.text = text
}
}
}
statisticsTimer = Timer("${TAG}::Statistics").apply {
schedule(task, 0, 1000)
}
}
}
private fun stopStatistics() {
if (DEBUG) Log.v(TAG, "stopStatistics()")
statisticsTimer?.apply {
Log.i(TAG, "Stop statistics")
cancel()
}
statisticsTimer = null
}
private fun setKeepScreenOn(enable: Boolean) {
if (DEBUG) Log.v(TAG, "setKeepScreenOn(enable=$enable)")
if (enable) {
activity?.apply {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
Log.i(TAG, "Enabled keep screen on")
}
} else {
activity?.apply {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
Log.i(TAG, "Disabled keep screen on")
}
}
}
companion object {
private val TAG: String = LiveFragment::class.java.simpleName
private const val DEBUG = true
}
}
================================================
FILE: app/src/main/java/com/alexvas/rtsp/demo/live/LiveViewModel.kt
================================================
package com.alexvas.rtsp.demo.live
import android.annotation.SuppressLint
import android.content.Context
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.widget.EditText
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
private const val RTSP_REQUEST_KEY = "rtsp_request"
private const val RTSP_USERNAME_KEY = "rtsp_username"
private const val RTSP_PASSWORD_KEY = "rtsp_password"
private const val DEFAULT_RTSP_REQUEST = "rtsp://10.0.1.3:554/axis-media/media.amp"
private const val DEFAULT_RTSP_USERNAME = ""
private const val DEFAULT_RTSP_PASSWORD = ""
private const val LIVE_PARAMS_FILENAME = "live_params"
@SuppressLint("LogNotTimber")
class LiveViewModel : ViewModel() {
val rtspRequest = MutableLiveData<String>().apply {
value = DEFAULT_RTSP_REQUEST
}
val rtspUsername = MutableLiveData<String>().apply {
value = DEFAULT_RTSP_USERNAME
}
val rtspPassword = MutableLiveData<String>().apply {
value = DEFAULT_RTSP_PASSWORD
}
// private val _text = MutableLiveData<String>().apply {
// value = "This is live Fragment"
// }
// val text: LiveData<String> = _text
// init {
// // Here you could use the ID to get the user info from the DB or remote server
// rtspRequest.value = "rtsp://10.0.1.3:554/axis-media/media.amp"
// }
fun loadParams(context: Context) {
if (DEBUG) Log.v(TAG, "loadParams()")
val pref = context.getSharedPreferences(LIVE_PARAMS_FILENAME, Context.MODE_PRIVATE)
try {
rtspRequest.setValue(pref.getString(RTSP_REQUEST_KEY, DEFAULT_RTSP_REQUEST))
} catch (e: ClassCastException) {
e.printStackTrace()
}
try {
rtspUsername.setValue(pref.getString(RTSP_USERNAME_KEY, DEFAULT_RTSP_USERNAME))
} catch (e: ClassCastException) {
e.printStackTrace()
}
try {
rtspPassword.setValue(pref.getString(RTSP_PASSWORD_KEY, DEFAULT_RTSP_PASSWORD))
} catch (e: ClassCastException) {
e.printStackTrace()
}
}
fun saveParams(context: Context) {
if (DEBUG) Log.v(TAG, "saveParams()")
context.getSharedPreferences(LIVE_PARAMS_FILENAME, Context.MODE_PRIVATE).edit().apply {
putString(RTSP_REQUEST_KEY, rtspRequest.value)
putString(RTSP_USERNAME_KEY, rtspUsername.value)
putString(RTSP_PASSWORD_KEY, rtspPassword.value)
apply()
}
}
fun initEditTexts(etRtspRequest: EditText, etRtspUsername: EditText, etRtspPassword: EditText) {
if (DEBUG) Log.v(TAG, "initEditTexts()")
etRtspRequest.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
val text = s.toString()
if (text != rtspRequest.value) {
rtspRequest.value = text
}
}
})
etRtspUsername.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
val text = s.toString()
if (text != rtspUsername.value) {
rtspUsername.value = text
}
}
})
etRtspPassword.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
val text = s.toString()
if (text != rtspPassword.value) {
rtspPassword.value = text
}
}
})
}
companion object {
private val TAG: String = LiveViewModel::class.java.simpleName
private const val DEBUG = false
}
}
================================================
FILE: app/src/main/java/com/alexvas/rtsp/demo/live/RawFragment.kt
================================================
package com.alexvas.rtsp.demo.live
import android.annotation.SuppressLint
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.alexvas.rtsp.RtspClient
import com.alexvas.rtsp.demo.databinding.FragmentRawBinding
import com.alexvas.rtsp.widget.toHexString
import com.alexvas.utils.NetUtils
import kotlinx.coroutines.Runnable
import java.net.Socket
import java.util.Timer
import java.util.TimerTask
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.math.min
@SuppressLint("LogNotTimber")
class RawFragment : Fragment() {
private lateinit var binding: FragmentRawBinding
private lateinit var liveViewModel: LiveViewModel
private var statisticsTimer: Timer? = null
private val rtspStopped = AtomicBoolean(true)
private var rtspVideoBytesReceived: Long = 0
private var rtspVideoFramesReceived: Long = 0
private var rtspAudioBytesReceived: Long = 0
private var rtspAudioSamplesReceived: Long = 0
private var rtspApplicationBytesReceived: Long = 0
private var rtspApplicationSamplesReceived: Long = 0
private val rtspClientListener = object: RtspClient.RtspClientListener {
override fun onRtspConnecting() {
if (DEBUG) Log.v(TAG, "onRtspConnecting()")
rtspVideoBytesReceived = 0
rtspVideoFramesReceived = 0
rtspAudioBytesReceived = 0
rtspAudioSamplesReceived = 0
rtspApplicationBytesReceived = 0
rtspApplicationSamplesReceived = 0
binding.apply {
root.post {
updateStatistics()
llRtspParams.etRtspRequest.isEnabled = false
llRtspParams.etRtspUsername.isEnabled = false
llRtspParams.etRtspPassword.isEnabled = false
llRtspParams.cbVideo.isEnabled = false
llRtspParams.cbAudio.isEnabled = false
llRtspParams.cbApplication.isEnabled = false
llRtspParams.cbDebug.isEnabled = false
tvStatusSurface.text = "RTSP connecting"
bnStartStop.text = "Stop RTSP"
}
}
}
override fun onRtspConnected(sdpInfo: RtspClient.SdpInfo) {
if (DEBUG) Log.v(TAG, "onRtspConnected()")
binding.apply {
root.post {
tvStatusSurface.text = "RTSP connected"
}
}
startStatistics()
}
override fun onRtspVideoNalUnitReceived(data: ByteArray, offset: Int, length: Int, timestamp: Long) {
val numBytesDump = min(length, 25) // dump max 25 bytes
Log.i(TAG, "RTSP video data ($length bytes): ${data.toHexString(offset, offset + numBytesDump)}")
rtspVideoBytesReceived += length
rtspVideoFramesReceived++
}
override fun onRtspAudioSampleReceived(data: ByteArray, offset: Int, length: Int, timestamp: Long) {
val numBytesDump = min(length, 25) // dump max 25 bytes
Log.i(TAG, "RTSP audio data ($length bytes): ${data.toHexString(offset, offset + numBytesDump)}")
rtspAudioBytesReceived += length
rtspAudioSamplesReceived++
}
override fun onRtspApplicationDataReceived(data: ByteArray, offset: Int, length: Int, timestamp: Long) {
val numBytesDump = min(length, 25) // dump max 25 bytes
Log.i(TAG, "RTSP app data ($length bytes): ${data.toHexString(offset, offset + numBytesDump)}")
rtspApplicationBytesReceived += length
rtspApplicationSamplesReceived++
}
override fun onRtspDisconnecting() {
if (DEBUG) Log.v(TAG, "onRtspDisconnecting()")
binding.apply {
root.post {
tvStatusSurface.text = "RTSP disconnecting"
}
}
stopStatistics()
}
override fun onRtspDisconnected() {
if (DEBUG) Log.v(TAG, "onRtspDisconnected()")
binding.apply {
root.post {
tvStatusSurface.text = "RTSP disconnected"
bnStartStop.text = "Start RTSP"
llRtspParams.cbVideo.isEnabled = true
llRtspParams.cbAudio.isEnabled = true
llRtspParams.cbApplication.isEnabled = true
llRtspParams.cbDebug.isEnabled = true
llRtspParams.etRtspRequest.isEnabled = true
llRtspParams.etRtspUsername.isEnabled = true
llRtspParams.etRtspPassword.isEnabled = true
}
}
}
override fun onRtspFailedUnauthorized() {
if (DEBUG) Log.e(TAG, "onRtspFailedUnauthorized()")
Log.e(TAG, "RTSP failed unauthorized")
if (context == null) return
onRtspDisconnected()
binding.apply {
root.post {
tvStatusSurface.text = "RTSP username or password invalid"
}
}
}
override fun onRtspFailed(message: String?) {
if (DEBUG) Log.e(TAG, "onRtspFailed(message='$message')")
Log.e(TAG, "RTSP failed with message '$message'")
if (context == null) return
onRtspDisconnected()
binding.apply {
root.post {
tvStatusSurface.text = "Error: $message"
}
}
}
}
private val threadRunnable = Runnable {
Log.i(TAG, "Thread started")
var socket: Socket? = null
try {
val uri = Uri.parse(liveViewModel.rtspRequest.value)
val port = if (uri.port == -1) DEFAULT_RTSP_PORT else uri.port
socket = NetUtils.createSocketAndConnect(uri.host!!, port, 5000)
val rtspClient =
RtspClient.Builder(
socket,
uri.toString(),
rtspStopped,
rtspClientListener
)
.requestVideo(binding.llRtspParams.cbVideo.isChecked)
.requestAudio(binding.llRtspParams.cbAudio.isChecked)
.requestApplication(binding.llRtspParams.cbApplication.isChecked)
.withDebug(binding.llRtspParams.cbDebug.isChecked)
.withUserAgent("rtsp-client-android")
.withCredentials(
binding.llRtspParams.etRtspUsername.text.toString(),
binding.llRtspParams.etRtspPassword.text.toString())
.build()
rtspClient.execute()
} catch (e: Exception) {
e.printStackTrace()
binding.root.post { rtspClientListener.onRtspFailed(e.message) }
} finally {
NetUtils.closeSocket(socket)
}
Log.i(TAG, "Thread stopped")
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
if (DEBUG) Log.v(TAG, "onCreateView()")
liveViewModel = ViewModelProvider(this)[LiveViewModel::class.java]
binding = FragmentRawBinding.inflate(inflater, container, false)
liveViewModel.initEditTexts(
binding.llRtspParams.etRtspRequest,
binding.llRtspParams.etRtspUsername,
binding.llRtspParams.etRtspPassword
)
liveViewModel.rtspRequest.observe(viewLifecycleOwner) {
if (binding.llRtspParams.etRtspRequest.text.toString() != it)
binding.llRtspParams.etRtspRequest.setText(it)
}
liveViewModel.rtspUsername.observe(viewLifecycleOwner) {
if (binding.llRtspParams.etRtspUsername.text.toString() != it)
binding.llRtspParams.etRtspUsername.setText(it)
}
liveViewModel.rtspPassword.observe(viewLifecycleOwner) {
if (binding.llRtspParams.etRtspPassword.text.toString() != it)
binding.llRtspParams.etRtspPassword.setText(it)
}
binding.bnStartStop.setOnClickListener {
if (DEBUG) Log.v(TAG, "onClick() rtspStopped=${rtspStopped.get()}")
if (rtspStopped.get()) {
rtspStopped.set(false)
Log.i(TAG, "Thread starting...")
Thread(threadRunnable).apply {
name = "RTSP raw thread"
start()
}
} else {
Log.i(TAG, "Thread stopping...")
rtspStopped.set(true)
}
}
return binding.root
}
override fun onResume() {
if (DEBUG) Log.v(TAG, "onResume()")
super.onResume()
liveViewModel.loadParams(requireContext())
}
override fun onPause() {
if (DEBUG) Log.v(TAG, "onPause()")
super.onPause()
liveViewModel.saveParams(requireContext())
stopStatistics()
rtspStopped.set(true)
}
private fun updateStatistics() {
// if (DEBUG) Log.v(TAG, "updateStatistics()")
binding.apply {
tvStatisticsVideo.text = "Video: $rtspVideoBytesReceived bytes, $rtspVideoFramesReceived frames"
tvStatisticsAudio.text = "Audio: $rtspAudioBytesReceived bytes, $rtspAudioSamplesReceived samples"
tvStatisticsApplication.text = "Application: $rtspApplicationBytesReceived bytes, $rtspApplicationSamplesReceived samples"
}
}
private fun startStatistics() {
if (DEBUG) Log.v(TAG, "startStatistics()")
Log.i(TAG, "Start statistics")
if (statisticsTimer == null) {
val task: TimerTask = object : TimerTask() {
override fun run() {
binding.root.post {
updateStatistics()
}
}
}
statisticsTimer = Timer("${TAG}::Statistics").apply {
schedule(task, 0, 1000)
}
}
}
private fun stopStatistics() {
if (DEBUG) Log.v(TAG, "stopStatistics()")
statisticsTimer?.apply {
Log.i(TAG, "Stop statistics")
cancel()
}
statisticsTimer = null
}
companion object {
private val TAG: String = RawFragment::class.java.simpleName
private const val DEBUG = true
private const val DEFAULT_RTSP_PORT = 554
}
}
================================================
FILE: app/src/main/res/drawable/ic_camera_black_24dp.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M4,4H7L9,2H15L17,4H20A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20H4A2,2 0 0,1 2,18V6A2,2 0 0,1 4,4M12,7A5,5 0 0,0 7,12A5,5 0 0,0 12,17A5,5 0 0,0 17,12A5,5 0 0,0 12,7M12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9Z" />
</vector>
================================================
FILE: app/src/main/res/drawable/ic_cctv_black_24dp.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M18.15,4.94C17.77,4.91 17.37,5 17,5.2L8.35,10.2C7.39,10.76 7.07,12 7.62,12.94L9.12,15.53C9.67,16.5 10.89,16.82 11.85,16.27L13.65,15.23C13.92,15.69 14.32,16.06 14.81,16.27V18.04C14.81,19.13 15.7,20 16.81,20H22V18.04H16.81V16.27C17.72,15.87 18.31,14.97 18.31,14C18.31,13.54 18.19,13.11 17.97,12.73L20.5,11.27C21.47,10.71 21.8,9.5 21.24,8.53L19.74,5.94C19.4,5.34 18.79,5 18.15,4.94M6.22,13.17L2,13.87L2.75,15.17L4.75,18.63L5.5,19.93L8.22,16.63L6.22,13.17Z" />
</vector>
================================================
FILE: app/src/main/res/drawable/ic_launcher_background.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
================================================
FILE: app/src/main/res/drawable/ic_launcher_foreground.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="16dp"
android:width="16dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#ffffff" android:pathData="M18.15,4.94C17.77,4.91 17.37,5 17,5.2L8.35,10.2C7.39,10.76 7.07,12 7.62,12.94L9.12,15.53C9.67,16.5 10.89,16.82 11.85,16.27L13.65,15.23C13.92,15.69 14.32,16.06 14.81,16.27V18.04C14.81,19.13 15.7,20 16.81,20H22V18.04H16.81V16.27C17.72,15.87 18.31,14.97 18.31,14C18.31,13.54 18.19,13.11 17.97,12.73L20.5,11.27C21.47,10.71 21.8,9.5 21.24,8.53L19.74,5.94C19.4,5.34 18.79,5 18.15,4.94M6.22,13.17L2,13.87L2.75,15.17L4.75,18.63L5.5,19.93L8.22,16.63L6.22,13.17Z" />
</vector>
================================================
FILE: app/src/main/res/drawable/ic_text_subject_black_24dp.xml
================================================
<!-- drawable/text_subject.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M4,5H20V7H4V5M4,9H20V11H4V9M4,13H20V15H4V13M4,17H14V19H4V17Z" />
</vector>
================================================
FILE: app/src/main/res/layout/activity_main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:layout_above="@+id/nav_view"
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/mobile_navigation" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/windowBackground"
android:layout_alignParentBottom="true"
app:menu="@menu/bottom_nav_menu" />
</RelativeLayout>
================================================
FILE: app/src/main/res/layout/fragment_live.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".live.LiveFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<include
android:id="@+id/llRtspParams"
layout="@layout/layout_rtsp_params"/>
<CheckBox
android:id="@+id/cbVideoFpsStabilization"
android:text="Video frame rate stabilization.\nAdd delay up to 100ms for smoother playback. RtspSurfaceView only."
android:layout_marginStart="5dp"
android:checked="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<CheckBox
android:id="@+id/cbExperimentalRewriteSps"
android:text="Rewrite SPS frames w/ low-latency params (EXPERIMENTAL)"
android:checked="false"
android:layout_marginStart="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:layout_marginTop="40dp"
android:id="@+id/bnStartStopSurface"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start" />
<!-- RtspSurfaceView -->
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingBottom="5dp"
android:text="RtspSurfaceView:"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/csVideoSurface"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true">
<com.alexvas.rtsp.widget.RtspSurfaceView
android:layout_width="0dp"
android:layout_height="0dp"
android:id="@+id/svVideoSurface"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintDimensionRatio="16:9"/>
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@android:color/black"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:id="@+id/vShutterSurface" />
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
android:id="@+id/pbLoadingSurface"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Debug statistics -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:id="@+id/tvStatistics"
android:textSize="12sp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.Material3.Button.TextButton.Icon"
android:id="@+id/bnSnapshotSurface"
android:enabled="false"
android:text="Snapshot"
app:icon="@drawable/ic_camera_black_24dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/tvStatusSurface"
android:gravity="end"/>
</LinearLayout>
<!-- RtspImageView -->
<Button
android:layout_marginTop="30dp"
android:id="@+id/bnStartStopImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Start" />
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:paddingBottom="5dp"
android:text="RtspImageView:"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/csVideoImage"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true">
<com.alexvas.rtsp.widget.RtspImageView
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="fitXY"
android:id="@+id/ivVideoImage"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintDimensionRatio="16:9"/>
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@android:color/black"
android:id="@+id/vShutterImage"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone"
android:id="@+id/pbLoadingImage"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/tvStatusImage"
android:gravity="end"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Video decoder" />
<com.google.android.material.button.MaterialButtonToggleGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/bnVideoDecoderGroup"
android:layout_gravity="center"
app:singleSelection="true">
<Button
android:id="@+id/bnVideoDecoderHardware"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hardware" />
<Button
android:id="@+id/bnVideoDecoderSoftware"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Software" />
</com.google.android.material.button.MaterialButtonToggleGroup>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="10dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rotation" />
<com.google.android.material.button.MaterialButtonToggleGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:id="@+id/tgRotation"
app:singleSelection="true">
<Button
android:id="@+id/bnRotate0"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0" />
<Button
android:id="@+id/bnRotate90"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="90" />
<Button
android:id="@+id/bnRotate180"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="180" />
<Button
android:id="@+id/bnRotate270"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="270" />
</com.google.android.material.button.MaterialButtonToggleGroup>
</LinearLayout>
</LinearLayout>
</ScrollView>
================================================
FILE: app/src/main/res/layout/fragment_logs.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="8dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/log_recycler"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:scrollbars="vertical" />
</HorizontalScrollView>
================================================
FILE: app/src/main/res/layout/fragment_raw.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".live.LiveFragment">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<include
android:id="@+id/llRtspParams"
layout="@layout/layout_rtsp_params" />
<Button
android:layout_marginTop="10dp"
android:id="@+id/bnStartStop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="10dp"
android:text="Start" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/tvStatusSurface"
android:gravity="end"/>
<!-- Debug statistics -->
<TextView
android:id="@+id/tvStatisticsVideo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:padding="5dp"
android:textStyle="normal|bold"
android:textSize="16sp"/>
<TextView
android:id="@+id/tvStatisticsAudio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:textStyle="normal|bold"
android:textSize="16sp"/>
<TextView
android:id="@+id/tvStatisticsApplication"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="5dp"
android:textStyle="normal|bold"
android:textSize="16sp"/>
</LinearLayout>
</ScrollView>
================================================
FILE: app/src/main/res/layout/layout_rtsp_params.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp">
<EditText
android:id="@+id/etRtspRequest"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="RTSP request"
android:inputType="textUri"/>
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:orientation="horizontal">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="5dp">
<EditText
android:id="@+id/etRtspUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="RTSP username"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
app:passwordToggleEnabled="true">
<EditText
android:id="@+id/etRtspPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:hint="RTSP password"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<CheckBox
android:id="@+id/cbVideo"
android:text="Video"
android:checked="true"
android:layout_margin="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<CheckBox
android:id="@+id/cbAudio"
android:text="Audio"
android:checked="false"
android:layout_margin="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<CheckBox
android:id="@+id/cbApplication"
android:text="Application"
android:checked="false"
android:layout_margin="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<CheckBox
android:id="@+id/cbDebug"
android:text="Debug"
android:checked="false"
android:layout_marginStart="20dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
================================================
FILE: app/src/main/res/menu/bottom_nav_menu.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_live"
android:icon="@drawable/ic_cctv_black_24dp"
android:title="@string/title_live" />
<item
android:id="@+id/navigation_raw"
android:icon="@drawable/ic_cctv_black_24dp"
android:title="@string/title_raw" />
<item
android:id="@+id/navigation_logs"
android:icon="@drawable/ic_text_subject_black_24dp"
android:title="@string/title_logs" />
</menu>
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
================================================
FILE: app/src/main/res/navigation/mobile_navigation.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mobile_navigation"
app:startDestination="@+id/navigation_live">
<fragment
android:id="@+id/navigation_live"
android:name="com.alexvas.rtsp.demo.live.LiveFragment"
android:label="@string/title_live"
tools:layout="@layout/fragment_live" />
<fragment
android:id="@+id/navigation_raw"
android:name="com.alexvas.rtsp.demo.live.RawFragment"
android:label="@string/title_live"
tools:layout="@layout/fragment_live" />
<fragment
android:id="@+id/navigation_logs"
android:name="info.hannes.logcat.ui.LogcatFragment"
android:label="@string/title_logs"
tools:layout="@layout/fragment_log" />
</navigation>
================================================
FILE: app/src/main/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#40747A</color>
<color name="colorPrimaryDark">#00BCD4</color>
<color name="colorAccent">#03DAC5</color>
</resources>
================================================
FILE: app/src/main/res/values/dimens.xml
================================================
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
</resources>
================================================
FILE: app/src/main/res/values/strings.xml
================================================
<resources>
<string name="app_name">Rtsp demo</string>
<string name="title_live">Live</string>
<string name="title_raw">Raw</string>
<string name="title_logs">Logs</string>
</resources>
================================================
FILE: app/src/main/res/values/styles.xml
================================================
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.Material3.DayNight">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimaryDark</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
================================================
FILE: build.gradle
================================================
buildscript {
ext.kotlin_version = '2.2.21'
ext.compile_sdk_version = 36
ext.min_sdk_version = 24
ext.target_sdk_version = 35
ext.project_version_code = 564
ext.project_version_name = '5.6.4'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}
tasks.register('clean', Delete) {
delete rootProject.layout.buildDirectory
}
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
================================================
FILE: gradle.properties
================================================
org.gradle.jvmargs=-Xmx1g
android.useAndroidX=true
================================================
FILE: gradlew
================================================
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: jitpack.yml
================================================
jdk:
- openjdk17
install:
- ./gradlew build :library-client-rtsp:publishToMavenLocal
================================================
FILE: library-client-rtsp/.gitignore
================================================
# Created by https://www.gitignore.io/api/android,java,intellij
### Android ###
# Built application files
*.apk
*.ap_
# Files for the Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
xactmobile/class_files.txt
xactmobile/mapping.txt
xactmobile/seeds.txt
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
### Android Patch ###
gen-external-apklibs
### Java ###
*.class
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
#*.jar
*.war
*.ear
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
*.iml
## Directory-based project format:
.idea/
# if you remove the above rule, at least ignore the following:
# User-specific stuff:
.idea/workspace.xml
.idea/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/dataSources.ids
.idea/dataSources.xml
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
# Gradle:
.idea/gradle.xml
.idea/libraries
# Mongo Explorer plugin:
.idea/mongoSettings.xml
## File-based project format:
*.ipr
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
xactmobile/.DS_Store~64be78fe3602626c61b52bcbfd09e09a6107b50a
xactmobile/.DS_Store~HEAD
oslab-viewpager/._.DS_Store
oslab-viewpager/src/main/.DS_Store
oslab-viewpager/src/main/._.DS_Store
oslab-viewpager/src/main/res/.DS_Store
oslab-viewpager/src/main/res/._.DS_Store
oslab-viewpager/.gitignore
oslab-materialdesign/.DS_Store
oslab-materialdesign/._.DS_Store
oslab-materialdesign/src/.DS_Store
oslab-materialdesign/src/._.DS_Store
oslab-materialdesign/src/main/.DS_Store
oslab-materialdesign/src/main/._.DS_Store
oslab-materialdesign/src/main/res/.DS_Store
oslab-materialdesign/src/main/res/._.DS_Store
================================================
FILE: library-client-rtsp/build.gradle
================================================
plugins {
id 'com.android.library'
id 'kotlin-android'
id 'maven-publish'
}
apply plugin: 'com.android.library'
project.afterEvaluate {
publishing {
publications {
release(MavenPublication) {
from components.release
}
}
}
}
android {
compileSdkVersion compile_sdk_version
defaultConfig {
minSdk min_sdk_version
targetSdk target_sdk_version
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
namespace 'com.alexvas.rtsp'
}
dependencies {
implementation 'androidx.annotation:annotation:1.9.1'
implementation 'androidx.media3:media3-exoplayer:1.9.3'
implementation 'androidx.camera:camera-core:1.5.3' // YUV -> BMP conversion
implementation 'org.jcodec:jcodec:0.2.5' // SPS frame modification
}
================================================
FILE: library-client-rtsp/proguard-rules.txt
================================================
# Proguard rules.
================================================
FILE: library-client-rtsp/src/main/AndroidManifest.xml
================================================
<manifest
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
================================================
FILE: library-client-rtsp/src/main/java/com/alexvas/rtsp/RtspClient.java
================================================
package com.alexvas.rtsp;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.alexvas.rtsp.parser.AacParser;
import com.alexvas.rtsp.parser.G711Parser;
import com.alexvas.rtsp.parser.AudioParser;
import com.alexvas.rtsp.parser.RtpH264Parser;
import com.alexvas.rtsp.parser.RtpH265Parser;
import com.alexvas.rtsp.parser.RtpHeaderParser;
import com.alexvas.rtsp.parser.RtpParser;
import com.alexvas.utils.NetUtils;
import com.alexvas.utils.VideoCodecUtils;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serial;
import java.math.BigInteger;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
//OPTIONS rtsp://10.0.1.145:88/videoSub RTSP/1.0
//CSeq: 1
//User-Agent: Lavf58.29.100
//
//RTSP/1.0 200 OK
//CSeq: 1
//Date: Fri, Jan 03 2020 22:03:07 GMT
//Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER
//DESCRIBE rtsp://10.0.1.145:88/videoSub RTSP/1.0
//Accept: application/sdp
//CSeq: 2
//User-Agent: Lavf58.29.100
//
//RTSP/1.0 401 Unauthorized
//CSeq: 2
//Date: Fri, Jan 03 2020 22:03:07 GMT
//WWW-Authenticate: Digest realm="Foscam IPCam Living Video", nonce="3c889dbf8371d3660aa2496789a5d130"
//DESCRIBE rtsp://10.0.1.145:88/videoSub RTSP/1.0
//Accept: application/sdp
//CSeq: 3
//User-Agent: Lavf58.29.100
//Authorization: Digest username="admin", realm="Foscam IPCam Living Video", nonce="3c889dbf8371d3660aa2496789a5d130", uri="rtsp://10.0.1.145:88/videoSub", response="4f062baec1c813ae3db15e3a14111d3d"
//
//RTSP/1.0 200 OK
//CSeq: 3
//Date: Fri, Jan 03 2020 22:03:07 GMT
//Content-Base: rtsp://10.0.1.145:65534/videoSub/
//Content-Type: application/sdp
//Content-Length: 495
//
//v=0
//o=- 1578088972261172 1 IN IP4 10.0.1.145
//s=IP Camera Video
//i=videoSub
//t=0 0
//a=tool:LIVE555 Streaming Media v2014.02.10
//a=type:broadcast
//a=control:*
//a=range:npt=0-
//a=x-qt-text-nam:IP Camera Video
//a=x-qt-text-inf:videoSub
//m=video 0 RTP/AVP 96
//c=IN IP4 0.0.0.0
//b=AS:96
//a=rtpmap:96 H264/90000
//a=fmtp:96 packetization-mode=1;profile-level-id=420020;sprop-parameter-sets=Z0IAIJWoFAHmQA==,aM48gA==
//a=control:track1
//m=audio 0 RTP/AVP 0
//c=IN IP4 0.0.0.0
//b=AS:64
//a=control:track2
//SETUP rtsp://10.0.1.145:65534/videoSub/track1 RTSP/1.0
//Transport: RTP/AVP/UDP;unicast;client_port=27452-27453
//CSeq: 4
//User-Agent: Lavf58.29.100
//Authorization: Digest username="admin", realm="Foscam IPCam Living Video", nonce="3c889dbf8371d3660aa2496789a5d130", uri="rtsp://10.0.1.145:65534/videoSub/track1", response="1fbc50b24d582c9331dd5e89f3102a06"
//
//RTSP/1.0 200 OK
//CSeq: 4
//Date: Fri, Jan 03 2020 22:03:07 GMT
//Transport: RTP/AVP;unicast;destination=10.0.1.53;source=10.0.1.145;client_port=27452-27453;server_port=6972-6973
//Session: 1F91B1B6;timeout=65
//SETUP rtsp://10.0.1.145:65534/videoSub/track2 RTSP/1.0
//Transport: RTP/AVP/UDP;unicast;client_port=27454-27455
//CSeq: 5
//User-Agent: Lavf58.29.100
//Session: 1F91B1B6
//Authorization: Digest username="admin", realm="Foscam IPCam Living Video", nonce="3c889dbf8371d3660aa2496789a5d130", uri="rtsp://10.0.1.145:65534/videoSub/track2", response="ad779abe070c096eff1012e7c70c986a"
//
//RTSP/1.0 200 OK
//CSeq: 5
//Date: Fri, Jan 03 2020 22:03:07 GMT
//Transport: RTP/AVP;unicast;destination=10.0.1.53;source=10.0.1.145;client_port=27454-27455;server_port=6974-6975
//Session: 1F91B1B6;timeout=65
//PLAY rtsp://10.0.1.145:65534/videoSub/ RTSP/1.0
//Range: npt=0.000-
//CSeq: 6
//User-Agent: Lavf58.29.100
//Session: 1F91B1B6
//Authorization: Digest username="admin", realm="Foscam IPCam Living Video", nonce="3c889dbf8371d3660aa2496789a5d130", uri="rtsp://10.0.1.145:65534/videoSub/", response="bb52eb6938dd4e50c4fac50363ffded0"
//
//RTSP/1.0 200 OK
//CSeq: 6
//Date: Fri, Jan 03 2020 22:03:07 GMT
//Range: npt=0.000-
//Session: 1F91B1B6
//RTP-Info: url=rtsp://10.0.1.145:65534/videoSub/track1;seq=42731;rtptime=2690581590,url=rtsp://10.0.1.145:65534/videoSub/track2;seq=34051;rtptime=3328043318
// https://www.ietf.org/rfc/rfc2326.txt
public class RtspClient {
private static final String TAG = RtspClient.class.getSimpleName();
static final String TAG_DEBUG = TAG + " DBG";
private static final boolean DEBUG = false;
private static final byte[] EMPTY_ARRAY = new byte[0];
public final static int RTSP_CAPABILITY_NONE = 0;
public final static int RTSP_CAPABILITY_OPTIONS = 1 << 1;
public final static int RTSP_CAPABILITY_DESCRIBE = 1 << 2;
public final static int RTSP_CAPABILITY_ANNOUNCE = 1 << 3;
public final static int RTSP_CAPABILITY_SETUP = 1 << 4;
public final static int RTSP_CAPABILITY_PLAY = 1 << 5;
public final static int RTSP_CAPABILITY_RECORD = 1 << 6;
public final static int RTSP_CAPABILITY_PAUSE = 1 << 7;
public final static int RTSP_CAPABILITY_TEARDOWN = 1 << 8;
public final static int RTSP_CAPABILITY_SET_PARAMETER = 1 << 9;
public final static int RTSP_CAPABILITY_GET_PARAMETER = 1 << 10;
public final static int RTSP_CAPABILITY_REDIRECT = 1 << 11;
public static boolean hasCapability(int capability, int capabilitiesMask) {
return (capabilitiesMask & capability) != 0;
}
public interface RtspClientListener {
void onRtspConnecting();
void onRtspConnected(@NonNull SdpInfo sdpInfo);
void onRtspVideoNalUnitReceived(@NonNull byte[] data, int offset, int length, long timestamp);
void onRtspAudioSampleReceived(@NonNull byte[] data, int offset, int length, long timestamp);
void onRtspApplicationDataReceived(@NonNull byte[] data, int offset, int length, long timestamp);
void onRtspDisconnecting();
void onRtspDisconnected();
void onRtspFailedUnauthorized();
void onRtspFailed(@Nullable String message);
}
private interface RtspClientKeepAliveListener {
void onRtspKeepAliveRequested();
}
public static class SdpInfo {
/**
* Session name (RFC 2327). In most cases RTSP server name.
*/
public @Nullable String sessionName;
/**
* Session description (RFC 2327).
*/
public @Nullable String sessionDescription;
public @Nullable VideoTrack videoTrack;
public @Nullable AudioTrack audioTrack;
public @Nullable ApplicationTrack applicationTrack;
}
public abstract static class Track {
public String request;
public int payloadType;
@NonNull
@Override
public String toString() {
return "Track{request='" + request + "', payloadType=" + payloadType + '}';
}
}
public static final int VIDEO_CODEC_H264 = 0;
public static final int VIDEO_CODEC_H265 = 1;
public static class VideoTrack extends Track {
public int videoCodec = VIDEO_CODEC_H264;
public @Nullable byte[] sps; // Both H.264 and H.265
public @Nullable byte[] pps; // Both H.264 and H.265
public @Nullable byte[] vps; // H.265 only
}
public static final int AUDIO_CODEC_UNKNOWN = -1;
public static final int AUDIO_CODEC_AAC = 0;
public static final int AUDIO_CODEC_OPUS = 1;
public static final int AUDIO_CODEC_G711_ULAW = 2;
public static final int AUDIO_CODEC_G711_ALAW = 3;
@NonNull
private static String getAudioCodecName(int codec) {
return switch (codec) {
case AUDIO_CODEC_AAC -> "AAC";
case AUDIO_CODEC_OPUS -> "Opus";
case AUDIO_CODEC_G711_ULAW -> "G.711 uLaw";
case AUDIO_CODEC_G711_ALAW -> "G.711 aLaw";
default -> "Unknown";
};
}
public static class AudioTrack extends Track {
public int audioCodec = AUDIO_CODEC_UNKNOWN;
public int sampleRateHz; // 16000, 8000
public int channels; // 1 - mono, 2 - stereo
public String mode; // AAC-lbr, AAC-hbr
public @Nullable byte[] config; // config=1210fff15081ffdffc
}
public static class ApplicationTrack extends Track {
}
private static final String CRLF = "\r\n";
// Size of buffer for reading from the connection
private final static int MAX_LINE_SIZE = 4098;
private static class UnauthorizedException extends IOException {
UnauthorizedException() {
super("Unauthorized");
}
}
private final static class NoResponseHeadersException extends IOException {
@Serial
private static final long serialVersionUID = 1L;
}
private final @NonNull Socket rtspSocket;
private @NonNull String uriRtsp;
private final @NonNull AtomicBoolean exitFlag;
private final @NonNull RtspClientListener listener;
// private boolean sendOptionsCommand;
private final boolean requestVideo;
private final boolean requestAudio;
private final boolean requestApplication;
private final boolean debug;
private final @Nullable String username;
private final @Nullable String password;
private final @Nullable String userAgent;
private RtspClient(@NonNull RtspClient.Builder builder) {
rtspSocket = builder.rtspSocket;
uriRtsp = builder.uriRtsp;
exitFlag = builder.exitFlag;
listener = builder.listener;
// sendOptionsCommand = builder.sendOptionsCommand;
requestVideo = builder.requestVideo;
requestAudio = builder.requestAudio;
requestApplication = builder.requestApplication;
username = builder.username;
password = builder.password;
debug = builder.debug;
userAgent = builder.userAgent;
}
public void execute() {
if (DEBUG) Log.v(TAG, "execute()");
listener.onRtspConnecting();
try {
final InputStream inputStream = rtspSocket.getInputStream();
final OutputStream outputStream = debug ?
new LoggerOutputStream(rtspSocket.getOutputStream()) :
new BufferedOutputStream(rtspSocket.getOutputStream());
SdpInfo sdpInfo = new SdpInfo();
final AtomicInteger cSeq = new AtomicInteger(0);
ArrayList<Pair<String, String>> headers;
int status;
String authToken = null;
Pair<String, String> digestRealmNonce = null;
// OPTIONS rtsp://10.0.1.78:8080/video/h264 RTSP/1.0
// CSeq: 1
// User-Agent: Lavf58.29.100
// RTSP/1.0 200 OK
// CSeq: 1
// Public: OPTIONS, DESCRIBE, SETUP, PLAY, GET_PARAMETER, SET_PARAMETER, TEARDOWN
// if (sendOptionsCommand) {
checkExitFlag(exitFlag);
sendOptionsCommand(outputStream, uriRtsp, cSeq.addAndGet(1), userAgent, null);
status = readResponseStatusCode(inputStream);
headers = readResponseHeaders(inputStream);
dumpHeaders(headers);
// Try once again with credentials
if (status == 401) {
digestRealmNonce = getHeaderWwwAuthenticateDigestRealmAndNonce(headers);
if (digestRealmNonce == null) {
String basicRealm = getHeaderWwwAuthenticateBasicRealm(headers);
if (TextUtils.isEmpty(basicRealm)) {
throw new IOException("Unknown authentication type");
}
// Basic auth
authToken = getBasicAuthHeader(username, password);
} else {
// Digest auth
authToken = getDigestAuthHeader(username, password, "OPTIONS", uriRtsp, digestRealmNonce.first, digestRealmNonce.second);
}
checkExitFlag(exitFlag);
sendOptionsCommand(outputStream, uriRtsp, cSeq.addAndGet(1), userAgent, authToken);
status = readResponseStatusCode(inputStream);
headers = readResponseHeaders(inputStream);
dumpHeaders(headers);
}
if (DEBUG)
Log.i(TAG, "OPTIONS status: " + status);
checkStatusCode(status);
final int capabilities = getSupportedCapabilities(headers);
// DESCRIBE rtsp://10.0.1.78:8080/video/h264 RTSP/1.0
// Accept: application/sdp
// CSeq: 2
// User-Agent: Lavf58.29.100
// RTSP/1.0 200 OK
// CSeq: 2
// Content-Type: application/sdp
// Content-Length: 364
//
// v=0
// t=0 0
// a=range:npt=now-
// m=video 0 RTP/AVP 96
// a=rtpmap:96 H264/90000
// a=fmtp:96 packetization-mode=1;sprop-parameter-sets=Z0KAH9oBABhpSCgwMDaFCag=,aM4G4g==
// a=control:trackID=1
// m=audio 0 RTP/AVP 96
// a=rtpmap:96 mpeg4-generic/48000/1
// a=fmtp:96 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1188
// a=control:trackID=2
checkExitFlag(exitFlag);
if (digestRealmNonce != null) {
authToken = getDigestAuthHeader(username, password, "DESCRIBE", uriRtsp, digestRealmNonce.first, digestRealmNonce.second);
}
sendDescribeCommand(outputStream, uriRtsp, cSeq.addAndGet(1), userAgent, authToken);
status = readResponseStatusCode(inputStream);
headers = readResponseHeaders(inputStream);
dumpHeaders(headers);
// Try once again with credentials. OPTIONS command can be accepted without authentication.
if (status == 401) {
digestRealmNonce = getHeaderWwwAuthenticateDigestRealmAndNonce(headers);
if (digestRealmNonce == null) {
String basicRealm = getHeaderWwwAuthenticateBasicRealm(headers);
if (TextUtils.isEmpty(basicRealm)) {
throw new IOException("Unknown authentication type");
}
// Basic auth
authToken = getBasicAuthHeader(username, password);
} else {
// Digest auth
authToken = getDigestAuthHeader(username, password, "DESCRIBE", uriRtsp, digestRealmNonce.first, digestRealmNonce.second);
}
checkExitFlag(exitFlag);
sendDescribeCommand(outputStream, uriRtsp, cSeq.addAndGet(1), userAgent, authToken);
status = readResponseStatusCode(inputStream);
headers = readResponseHeaders(inputStream);
dumpHeaders(headers);
}
if (DEBUG)
Log.i(TAG, "DESCRIBE status: " + status);
checkStatusCode(status);
String contentBaseUri = getHeaderContentBase(headers);
if (contentBaseUri != null) {
if (debug)
Log.i(TAG_DEBUG, "RTSP URI changed to '" + uriRtsp + "'");
uriRtsp = contentBaseUri;
}
int contentLength = getHeaderContentLength(headers);
if (contentLength > 0) {
String content = readContentAsText(inputStream, contentLength);
if (debug)
Log.i(TAG_DEBUG, "" + content);
try {
List<Pair<String, String>> params = getDescribeParams(content);
sdpInfo = getSdpInfoFromDescribeParams(params);
if (!requestVideo)
sdpInfo.videoTrack = null;
if (!requestAudio)
sdpInfo.audioTrack = null;
if (!requestApplication)
sdpInfo.applicationTrack = null;
// Only AAC supported
if (sdpInfo.audioTrack != null && sdpInfo.audioTrack.audioCodec == AUDIO_CODEC_UNKNOWN) {
Log.e(TAG_DEBUG, "Unknown RTSP audio codec (" + sdpInfo.audioTrack.audioCodec + ") specified in SDP");
sdpInfo.audioTrack = null;
}
} catch (Exception e) {
e.printStackTrace();
}
}
// SETUP rtsp://10.0.1.78:8080/video/h264/trackID=1 RTSP/1.0
// Transport: RTP/AVP/TCP;unicast;interleaved=0-1
// CSeq: 3
// User-Agent: Lavf58.29.100
// RTSP/1.0 200 OK
// CSeq: 3
// Transport: RTP/AVP/TCP;unicast;interleaved=0-1
// Session: Mzk5MzY2MzUwMTg3NTc2Mzc5NQ;timeout=30
String session = null;
int sessionTimeout = 0;
for (int i = 0; i < 3; i++) {
// 0 - video track, 1 - audio track, 2 - application track
checkExitFlag(exitFlag);
Track track;
switch (i) {
case 0 -> track = requestVideo ? sdpInfo.videoTrack : null;
case 1 -> track = requestAudio ? sdpInfo.audioTrack : null;
default -> track = requestApplication ? sdpInfo.applicationTrack : null;
}
if (track != null) {
String uriRtspSetup = getUriForSetup(uriRtsp, track);
if (uriRtspSetup == null) {
Log.e(TAG, "Failed to get RTSP URI for SETUP");
continue;
}
if (digestRealmNonce != null)
authToken = getDigestAuthHeader(
username,
password,
"SETUP",
uriRtspSetup,
digestRealmNonce.first,
digestRealmNonce.second);
sendSetupCommand(
outputStream,
uriRtspSetup,
cSeq.addAndGet(1),
userAgent,
authToken,
session,
(i == 0 ? "0-1" /*video*/ : "2-3" /*audio*/));
status = readResponseStatusCode(inputStream);
if (DEBUG)
Log.i(TAG, "SETUP status: " + status);
checkStatusCode(status);
headers = readResponseHeaders(inputStream);
dumpHeaders(headers);
session = getHeader(headers, "Session");
if (!TextUtils.isEmpty(session)) {
// ODgyODg3MjQ1MDczODk3NDk4Nw;timeout=30
String[] params = TextUtils.split(session, ";");
session = params[0];
// Getting session timeout
if (params.length > 1) {
params = TextUtils.split(params[1], "=");
if (params.length > 1) {
try {
sessionTimeout = Integer.parseInt(params[1]);
} catch (NumberFormatException e) {
Log.e(TAG, "Failed to parse RTSP session timeout");
}
}
}
}
if (DEBUG)
Log.d(TAG, "SETUP session: " + session + ", timeout: " + sessionTimeout);
if (TextUtils.isEmpty(session))
throw new IOException("Failed to get RTSP session");
}
}
if (TextUtils.isEmpty(session))
throw new IOException("Failed to get any media track");
// PLAY rtsp://10.0.1.78:8080/video/h264 RTSP/1.0
// Range: npt=0.000-
// CSeq: 5
// User-Agent: Lavf58.29.100
// Session: Mzk5MzY2MzUwMTg3NTc2Mzc5NQ
// RTSP/1.0 200 OK
// CSeq: 5
// RTP-Info: url=/video/h264;seq=56
// Session: Mzk5MzY2MzUwMTg3NTc2Mzc5NQ;timeout=30
checkExitFlag(exitFlag);
if (digestRealmNonce != null)
authToken = getDigestAuthHeader(username, password, "PLAY", uriRtsp /*?*/, digestRealmNonce.first, digestRealmNonce.second);
sendPlayCommand(outputStream, uriRtsp, cSeq.addAndGet(1), userAgent, authToken, session);
status = readResponseStatusCode(inputStream);
if (DEBUG)
Log.i(TAG, "PLAY status: " + status);
checkStatusCode(status);
headers = readResponseHeaders(inputStream);
dumpHeaders(headers);
listener.onRtspConnected(sdpInfo);
if (sdpInfo.videoTrack != null || sdpInfo.audioTrack != null || sdpInfo.applicationTrack != null) {
if (digestRealmNonce != null)
authToken = getDigestAuthHeader(username, password, hasCapability(RTSP_CAPABILITY_GET_PARAMETER, capabilities) ? "GET_PARAMETER" : "OPTIONS", uriRtsp, digestRealmNonce.first, digestRealmNonce.second);
final String authTokenFinal = authToken;
final String sessionFinal = session;
RtspClientKeepAliveListener keepAliveListener = () -> {
try {
//GET_PARAMETER rtsp://10.0.1.155:554/cam/realmonitor?channel=1&subtype=1/ RTSP/1.0
//CSeq: 6
//User-Agent: Lavf58.45.100
//Session: 4066342621205
//Authorization: Digest username="admin", realm="Login to cam", nonce="8fb58500489d60f99a40b43f3c8574ef", uri="rtsp://10.0.1.155:554/cam/realmonitor?channel=1&subtype=1/", response="692a26124a1ee9562135785ace33a23b"
//RTSP/1.0 200 OK
//CSeq: 6
//Session: 4066342621205
if (debug)
Log.d(TAG_DEBUG, "Sending keep-alive");
if (hasCapability(RTSP_CAPABILITY_GET_PARAMETER, capabilities))
sendGetParameterCommand(outputStream, uriRtsp, cSeq.addAndGet(1), userAgent, sessionFinal, authTokenFinal);
else
sendOptionsCommand(outputStream, uriRtsp, cSeq.addAndGet(1), userAgent, authTokenFinal);
// Do not read response right now, since it may contain unread RTP frames.
// RtpHeader.searchForNextRtpHeader will handle that.
} catch (IOException e) {
e.printStackTrace();
}
};
// Blocking call unless exitFlag set to true, thread.interrupt() called or connection closed.
try {
readRtpData(
inputStream,
sdpInfo,
exitFlag,
listener,
sessionTimeout / 2 * 1000,
keepAliveListener);
} finally {
// Cleanup resources on server side
if (hasCapability(RTSP_CAPABILITY_TEARDOWN, capabilities)) {
if (digestRealmNonce != null)
authToken = getDigestAuthHeader(username, password, "TEARDOWN", uriRtsp, digestRealmNonce.first, digestRealmNonce.second);
sendTeardownCommand(outputStream, uriRtsp, cSeq.addAndGet(1), userAgent, authToken, sessionFinal);
}
}
} else {
listener.onRtspFailed("No tracks found. RTSP server issue.");
}
listener.onRtspDisconnecting();
listener.onRtspDisconnected();
} catch (UnauthorizedException e) {
e.printStackTrace();
listener.onRtspFailedUnauthorized();
} catch (InterruptedException e) {
// Thread interrupted. Expected behavior.
listener.onRtspDisconnecting();
listener.onRtspDisconnected();
} catch (Exception e) {
e.printStackTrace();
listener.onRtspFailed(e.getMessage());
}
try {
rtspSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Nullable
private static String getUriForSetup(@NonNull String uriRtsp, @Nullable Track track) {
if (DEBUG) Log.v(TAG, "getUriForSetup(uriRtsp='" + uriRtsp + "', track=" + track + ")");
if (track == null)
return null;
if (track.request == null) {
// a=control:trackID=1 is missed
Log.w(TAG, "Track request is empty. Skipping it.");
track.request = uriRtsp;
}
String uriRtspSetup = uriRtsp;
// Absolute URL
if (track.request.startsWith("rtsp://") || track.request.startsWith("rtsps://")) {
uriRtspSetup = track.request;
// Relative URL
} else {
if (!track.request.startsWith("/") && !uriRtspSetup.endsWith("/")) {
track.request = "/" + track.request;
}
uriRtspSetup += track.request;
}
return uriRtspSetup.trim();
}
private static void checkExitFlag(@NonNull AtomicBoolean exitFlag) throws InterruptedException {
if (exitFlag.get())
throw new InterruptedException();
}
private static void checkStatusCode(int code) throws IOException {
switch (code) {
case 200:
break;
case 401:
throw new UnauthorizedException();
default:
throw new IOException("Invalid status code " + code);
}
}
private static void readRtpData(
@NonNull InputStream inputStream,
@NonNull SdpInfo sdpInfo,
@NonNull AtomicBoolean exitFlag,
@NonNull RtspClientListener listener,
int keepAliveTimeout,
@NonNull RtspClientKeepAliveListener keepAliveListener)
throws IOException {
byte[] data = EMPTY_ARRAY; // Usually not bigger than MTU = 15KB
final RtpParser videoParser = (sdpInfo.videoTrack != null && sdpInfo.videoTrack.videoCodec == VIDEO_CODEC_H265 ?
new RtpH265Parser() :
new RtpH264Parser());
final AudioParser audioParser = sdpInfo.audioTrack != null
? switch (sdpInfo.audioTrack.audioCodec) {
case AUDIO_CODEC_AAC -> new AacParser(sdpInfo.audioTrack.mode);
case AUDIO_CODEC_G711_ULAW,
AUDIO_CODEC_G711_ALAW -> new G711Parser();
default -> null;
}
: null;
byte[] nalUnitSps = (sdpInfo.videoTrack != null ? sdpInfo.videoTrack.sps : null);
byte[] nalUnitPps = (sdpInfo.videoTrack != null ? sdpInfo.videoTrack.pps : null);
byte[] nalUnitSei = EMPTY_ARRAY;
byte[] nalUnitAud = EMPTY_ARRAY;
int videoSeqNum = 0;
long keepAliveSent = System.currentTimeMillis();
while (!exitFlag.get()) {
RtpHeaderParser.RtpHeader header = RtpHeaderParser.readHeader(inputStream);
if (header == null) {
continue;
// throw new IOException("No RTP frames found");
}
// header.dumpHeader();
if (header.payloadSize > data.length)
data = new byte[header.payloadSize];
NetUtils.readData(inputStream, data, 0, header.payloadSize);
// Check if keep-alive should be sent
long l = System.currentTimeMillis();
if (keepAliveTimeout > 0 && l - keepAliveSent > keepAliveTimeout) {
keepAliveSent = l;
keepAliveListener.onRtspKeepAliveRequested();
}
// Video
if (sdpInfo.videoTrack != null && header.payloadType == sdpInfo.videoTrack.payloadType) {
if (videoSeqNum > header.sequenceNumber)
Log.w(TAG, "Invalid video seq num " + videoSeqNum + "/" + header.sequenceNumber);
videoSeqNum = header.sequenceNumber;
byte[] nalUnit;
// If extendion bit set in header, skip extension data
if (header.extension == 1) {
int skipBytes = ((data[2] & 0xFF) << 8 | (data[3] & 0xFF)) * 4 + 4;
nalUnit = videoParser.processRtpPacketAndGetNalUnit(Arrays.copyOfRange(data, skipBytes, data.length),
header.payloadSize - skipBytes, header.marker == 1);
} else {
nalUnit = videoParser.processRtpPacketAndGetNalUnit(data, header.payloadSize, header.marker == 1);
}
if (nalUnit != null) {
boolean isH265 = sdpInfo.videoTrack.videoCodec == VIDEO_CODEC_H265;
byte type = VideoCodecUtils.INSTANCE.getNalUnitType(nalUnit, 0, nalUnit.length, isH265);
// Log.i(TAG, "NAL u: " + VideoCodecUtils.INSTANCE.getH265NalUnitTypeString(type));
switch (type) {
case VideoCodecUtils.NAL_SPS:
nalUnitSps = nalUnit;
// Looks like there is NAL_IDR_SLICE as well. Send it now.
if (nalUnit.length > VideoCodecUtils.MAX_NAL_SPS_SIZE)
listener.onRtspVideoNalUnitReceived(nalUnit, 0, nalUnit.length, header.getTimestampMsec());
break;
case VideoCodecUtils.NAL_PPS:
nalUnitPps = nalUnit;
// Looks like there is NAL_IDR_SLICE as well. Send it now.
if (nalUnit.length > VideoCodecUtils.MAX_NAL_SPS_SIZE)
listener.onRtspVideoNalUnitReceived(nalUnit, 0, nalUnit.length, header.getTimestampMsec());
break;
case VideoCodecUtils.NAL_AUD:
nalUnitAud = nalUnit;
break;
case VideoCodecUtils.NAL_SEI:
nalUnitSei = nalUnit;
break;
case VideoCodecUtils.NAL_IDR_SLICE:
// Combine IDR with SPS/PPS
if (nalUnitSps != null && nalUnitPps != null) {
byte[] nalUnitSpsPpsIdr = new byte[nalUnitAud.length + nalUnitSps.length + nalUnitPps.length + nalUnitSei.length + nalUnit.length];
int offset = 0;
System.arraycopy(nalUnitSps, 0, nalUnitSpsPpsIdr, offset, nalUnitSps.length);
offset += nalUnitSps.length;
System.arraycopy(nalUnitPps, 0, nalUnitSpsPpsIdr, offset, nalUnitPps.length);
offset += nalUnitPps.length;
System.arraycopy(nalUnitAud, 0, nalUnitSpsPpsIdr, offset, nalUnitAud.length);
offset += nalUnitAud.length;
System.arraycopy(nalUnitSei, 0, nalUnitSpsPpsIdr, offset, nalUnitSei.length);
offset += nalUnitSei.length;
System.arraycopy(nalUnit, 0, nalUnitSpsPpsIdr, offset, nalUnit.length);
listener.onRtspVideoNalUnitReceived(nalUnitSpsPpsIdr, 0, nalUnitSpsPpsIdr.length, header.getTimestampMsec());
// listener.onRtspVideoNalUnitReceived(nalUnitSppPpsIdr, 0, nalUnitSppPpsIdr.length, System.currentTimeMillis());
// Send it only once
nalUnitSps = null;
nalUnitPps = null;
nalUnitSei = EMPTY_ARRAY;
nalUnitAud = EMPTY_ARRAY;
break;
}
default:
if (nalUnitSei.length == 0 && nalUnitAud.length == 0) {
listener.onRtspVideoNalUnitReceived(nalUnit, 0, nalUnit.length, header.getTimestampMsec());
} else {
byte[] nalUnitAudSeiSlice = new byte[nalUnitAud.length + nalUnitSei.length + nalUnit.length];
int offset = 0;
System.arraycopy(nalUnitAud, 0, nalUnitAudSeiSlice, offset, nalUnitAud.length);
offset += nalUnitAud.length;
System.arraycopy(nalUnitSei, 0, nalUnitAudSeiSlice, offset, nalUnitSei.length);
offset += nalUnitSei.length;
System.arraycopy(nalUnit, 0, nalUnitAudSeiSlice, offset, nalUnit.length);
listener.onRtspVideoNalUnitReceived(nalUnitAudSeiSlice, 0, nalUnitAudSeiSlice.length, header.getTimestampMsec());
nalUnitSei = EMPTY_ARRAY;
nalUnitAud = EMPTY_ARRAY;
}
}
}
// Audio
} else if (sdpInfo.audioTrack != null && header.payloadType == sdpInfo.audioTrack.payloadType) {
if (audioParser != null) {
byte[] sample = audioParser.processRtpPacketAndGetSample(data, header.payloadSize);
if (sample != null)
listener.onRtspAudioSampleReceived(sample, 0, sample.length, header.getTimestampMsec());
}
// Application
} else if (sdpInfo.applicationTrack != null && header.payloadType == sdpInfo.applicationTrack.payloadType) {
listener.onRtspApplicationDataReceived(data, 0, header.payloadSize, header.getTimestampMsec());
// Unknown
} else {
// https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml
if (DEBUG && header.payloadType >= 96 && header.payloadType <= 127)
Log.w(TAG, "Invalid RTP payload type " + header.payloadType);
}
}
}
private static void sendSimpleCommand(
@NonNull String command,
@NonNull OutputStream outputStream,
@NonNull String request,
int cSeq,
@Nullable String userAgent,
@Nullable String session,
@Nullable String authToken)
throws IOException {
outputStream.write((command + " " + request + " RTSP/1.0" + CRLF).getBytes());
if (authToken != null)
outputStream.write(("Authorization: " + authToken + CRLF).getBytes());
outputStream.write(("CSeq: " + cSeq + CRLF).getBytes());
if (userAgent != null)
outputStream.write(("User-Agent: " + userAgent + CRLF).getBytes());
if (session != null)
outputStream.write(("Session: " + session + CRLF).getBytes());
outputStream.write(CRLF.getBytes());
outputStream.flush();
}
private static void sendOptionsCommand(
@NonNull OutputStream outputStream,
@NonNull String request,
int cSeq,
@Nullable String userAgent,
@Nullable String authToken)
throws IOException {
if (DEBUG) Log.v(TAG, "sendOptionsCommand(request=\"" + request + "\", cSeq=" + cSeq + ")");
sendSimpleCommand("OPTIONS", outputStream, request, cSeq, userAgent, null, authToken);
}
private static void sendGetParameterCommand(
@NonNull OutputStream outputStream,
@NonNull String request,
int cSeq,
@Nullable String userAgent,
@Nullable String session,
@Nullable String authToken)
throws IOException {
if (DEBUG) Log.v(TAG, "sendGetParameterCommand(request=\"" + request + "\", cSeq=" + cSeq + ")");
sendSimpleCommand("GET_PARAMETER", outputStream, request, cSeq, userAgent, session, authToken);
}
private static void sendDescribeCommand(
@NonNull OutputStream outputStream,
@NonNull String request,
int cSeq,
@Nullable String userAgent,
@Nullable String authToken)
throws IOException {
if (DEBUG) Log.v(TAG, "sendDescribeCommand(request=\"" + request + "\", cSeq=" + cSeq + ")");
outputStream.write(("DESCRIBE " + request + " RTSP/1.0" + CRLF).getBytes());
outputStream.write(("Accept: application/sdp" + CRLF).getBytes());
if (authToken != null)
outputStream.write(("Authorization: " + authToken + CRLF).getBytes());
outputStream.write(("CSeq: " + cSeq + CRLF).getBytes());
if (userAgent != null)
outputStream.write(("User-Agent: " + userAgent + CRLF).getBytes());
outputStream.write(CRLF.getBytes());
outputStream.flush();
}
private static void sendTeardownCommand(
@NonNull OutputStream outputStream,
@NonNull String request,
int cSeq,
@Nullable String userAgent,
@Nullable String authToken,
@Nullable String session)
throws IOException {
if (DEBUG) Log.v(TAG, "sendTeardownCommand(request=\"" + request + "\", cSeq=" + cSeq + ")");
outputStream.write(("TEARDOWN " + request + " RTSP/1.0" + CRLF).getBytes());
if (authToken != null)
outputStream.write(("Authorization: " + authToken + CRLF).getBytes());
outputStream.write(("CSeq: " + cSeq + CRLF).getBytes());
if (userAgent != null)
outputStream.write(("User-Agent: " + userAgent + CRLF).getBytes());
if (session != null)
outputStream.write(("Session: " + session + CRLF).getBytes());
outputStream.write(CRLF.getBytes());
outputStream.flush();
}
private static void sendSetupCommand(
@NonNull OutputStream outputStream,
@NonNull String request,
int cSeq,
@Nullable String userAgent,
@Nullable String authToken,
@Nullable String session,
@NonNull String interleaved)
throws IOException {
if (DEBUG) Log.v(TAG, "sendSetupCommand(request=\"" + request + "\", cSeq=" + cSeq + ")");
outputStream.write(("SETUP " + request + " RTSP/1.0" + CRLF).getBytes());
outputStream.write(("Transport: RTP/AVP/TCP;unicast;interleaved=" + interleaved + CRLF).getBytes());
if (authToken != null)
outputStream.write(("Authorization: " + authToken + CRLF).getBytes());
outputStream.write(("CSeq: " + cSeq + CRLF).getBytes());
if (userAgent != null)
outputStream.write(("User-Agent: " + userAgent + CRLF).getBytes());
if (session != null)
outputStream.write(("Session: " + session + CRLF).getBytes());
outputStream.write(CRLF.getBytes());
outputStream.flush();
}
private static void sendPlayCommand(
@NonNull OutputStream outputStream,
@NonNull String request,
int cSeq,
@Nullable String userAgent,
@Nullable String authToken,
@NonNull String session)
throws IOException {
if (DEBUG) Log.v(TAG, "sendPlayCommand(request=\"" + request + "\", cSeq=" + cSeq + ")");
outputStream.write(("PLAY " + request + " RTSP/1.0" + CRLF).getBytes());
outputStream.write(("Range: npt=0.000-" + CRLF).getBytes());
if (authToken != null)
outputStream.write(("Authorization: " + authToken + CRLF).getBytes());
outputStream.write(("CSeq: " + cSeq + CRLF).getBytes());
if (userAgent != null)
outputStream.write(("User-Agent: " + userAgent + CRLF).getBytes());
outputStream.write(("Session: " + session + CRLF).getBytes());
outputStream.write(CRLF.getBytes());
outputStream.flush();
}
private int readResponseStatusCode(@NonNull InputStream inputStream) throws IOException {
// String line = readLine(inputStream);
// if (debug)
// Log.d(TAG_DEBUG, "" + line);
String line;
byte[] rtspHeader = "RTSP/1.0 ".getBytes();
// Search fpr "RTSP/1.0 "
while (!exitFlag.get() && readUntilBytesFound(inputStream, rtspHeader) && (line = readLine(inputStream)) != null) {
if (debug)
Log.d(TAG_DEBUG, "" + line);
// int indexRtsp = line.indexOf("TSP/1.0 "); // 8 characters, 'R' already found
// if (indexRtsp >= 0) {
int indexCode = line.indexOf(' ');
String code = line.substring(0, indexCode);
try {
int statusCode = Integer.parseInt(code);
// if (debug)
// Log.d(TAG_DEBUG, "Status code: " + statusCode);
return statusCode;
} catch (NumberFormatException e) {
// Does not fulfill standard "RTSP/1.1 200 OK" token
// Continue search for
}
// }
}
if (debug)
Log.w(TAG_DEBUG, "Could not obtain status code");
return -1;
}
@NonNull
private ArrayList<Pair<String, String>> readResponseHeaders(@NonNull InputStream inputStream) throws IOException {
ArrayList<Pair<String, String>> headers = new ArrayList<>();
String line;
while (!exitFlag.get() && !TextUtils.isEmpty(line = readLine(inputStream))) {
if (debug)
Log.d(TAG_DEBUG, "" + line);
if (CRLF.equals(line)) {
return headers;
} else {
String[] pairs = line.split(":", 2);
if (pairs.length == 2) {
headers.add(Pair.create(pairs[0].trim(), pairs[1].trim()));
}
}
}
return headers;
}
/**
* Get a list of tracks from SDP. Usually contains video and audio track only.
* @return array of 3 tracks. First is video track, second audio track, third application track.
*/
@NonNull
private static Track[] getTracksFromDescribeParams(@NonNull List<Pair<String, String>> params) {
Track[] tracks = new Track[3];
Track currentTrack = null;
for (Pair<String, String> param: params) {
switch (param.first) {
case "m":
// m=video 0 RTP/AVP 96
if (param.second.startsWith("video")) {
currentTrack = new VideoTrack();
tracks[0] = currentTrack;
// m=audio 0 RTP/AVP 97
// m=audio 0 RTP/AVP 0 8
} else if (param.second.startsWith("audio")) {
currentTrack = new AudioTrack();
tracks[1] = currentTrack;
// m=application 0 RTP/AVP 99
// a=rtpmap:99 com.my/90000
} else if (param.second.startsWith("application")) {
currentTrack = new ApplicationTrack();
tracks[2] = currentTrack;
} else if (param.second.startsWith("text")) {
Log.w(TAG, "Media track 'text' is not supported");
} else if (param.second.startsWith("message")) {
Log.w(TAG, "Media track 'message' is not supported");
} else {
currentTrack = null;
}
if (currentTrack != null) {
// m=<media> <port>/<number of ports> <proto> <fmt> ...
String[] values = TextUtils.split(param.second, " ");
try {
currentTrack.payloadType = (values.length > 3 ? Integer.parseInt(values[3]) : -1);
// Handle static PT that comes with no rtpmap
if (currentTrack instanceof AudioTrack track) {
switch (currentTrack.payloadType) {
case 0 -> { // uLaw
track.audioCodec = AUDIO_CODEC_G711_ULAW;
track.sampleRateHz = 8000;
track.channels = 1;
}
case 8 -> { // aLaw
track.audioCodec = AUDIO_CODEC_G711_ALAW;
track.sampleRateHz = 8000;
track.channels = 1;
}
}
}
} catch (Exception e) {
currentTrack.payloadType = -1;
}
if (currentTrack.payloadType == -1)
Log.e(TAG, "Failed to get payload type from \"m=" + param.second + "\"");
}
break;
case "a":
// a=control:trackID=1
if (currentTrack != null) {
if (param.second.startsWith("control:")) {
currentTrack.request = param.second.substring(8);
// a=fmtp:96 packetization-mode=1; profile-level-id=4D4029; sprop-parameter-sets=Z01AKZpmBkCb8uAtQEBAQXpw,aO48gA==
// a=fmtp:97 streamtype=5; profile-level-id=15; mode=AAC-hbr; config=1408; sizeLength=13; indexLength=3; indexDeltaLength=3; profile=1; bitrate=32000;
// a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
// a=fmtp:96 streamtype=5; profile-level-id=14; mode=AAC-lbr; config=1388; sizeLength=6; indexLength=2; indexDeltaLength=2; constantDuration=1024; maxDisplacement=5
// a=fmtp:96 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1210fff15081ffdffc
// a=fmtp:96
} else if (param.second.startsWith("fmtp:")) {
// Video
if (currentTrack instanceof VideoTrack) {
updateVideoTrackFromDescribeParam((VideoTrack)tracks[0], param);
// Audio
} else if (currentTrack instanceof AudioTrack) {
updateAudioTrackFromDescribeParam((AudioTrack)tracks[1], param);
}
// a=rtpmap:96 H264/90000
// a=rtpmap:97 mpeg4-generic/16000/1
// a=rtpmap:97 MPEG4-GENERIC/16000
// a=rtpmap:97 G726-32/8000
// a=rtpmap:96 mpeg4-generic/44100/2
} else if (param.second.startsWith("rtpmap:")) {
// Video
String[] values = TextUtils.split(param.second, " ");
if (currentTrack instanceof VideoTrack) {
if (values.length > 1) {
values = TextUtils.split(values[1], "/");
if (values.length > 0) {
switch (values[0].toLowerCase()) {
case "h264" -> ((VideoTrack) tracks[0]).videoCodec = VIDEO_CODEC_H264;
case "h265" -> ((VideoTrack) tracks[0]).videoCodec = VIDEO_CODEC_H265;
default -> Log.w(TAG, "Unknown video codec \"" + values[0] + "\"");
}
Log.i(TAG, "Video: " + values[0]);
}
}
// Audio
} else if (currentTrack instanceof AudioTrack) {
if (values.length > 1) {
AudioTrack track = ((AudioTrack) tracks[1]);
values = TextUtils.split(values[1], "/");
if (values.length > 1) {
switch (values[0].toLowerCase()) {
case "mpeg4-generic" -> track.audioCodec = AUDIO_CODEC_AAC;
case "opus" -> track.audioCodec = AUDIO_CODEC_OPUS;
case "pcmu" -> track.audioCodec = AUDIO_CODEC_G711_ULAW;
case "pcma" -> track.audioCodec = AUDIO_CODEC_G711_ALAW;
default -> {
Log.w(TAG, "Unknown audio codec \"" + values[0] + "\"");
track.audioCodec = AUDIO_CODEC_UNKNOWN;
}
}
track.sampleRateHz = Integer.parseInt(values[1]);
// If no channels specified, use mono, e.g. "a=rtpmap:97 MPEG4-GENERIC/8000"
track.channels = values.length > 2 ? Integer.parseInt(values[2]) : 1;
Log.i(TAG, "Audio: " + getAudioCodecName(track.audioCodec) + ", sample rate: " + track.sampleRateHz + " Hz, channels: " + track.channels);
}
}
// Application
} else {
// Do nothing
}
}
}
break;
}
}
return tracks;
}
//v=0
//o=- 1542237507365806 1542237507365806 IN IP4 10.0.1.111
//s=Media Presentation
//e=NONE
//b=AS:50032
//t=0 0
//a=control:*
//a=range:npt=0.000000-
//m=video 0 RTP/AVP 96
//c=IN IP4 0.0.0.0
//b=AS:50000
//a=framerate:25.0
//a=transform:1.000000,0.000000,0.000000;0.000000,1.000000,0.000000;0.000000,0.000000,1.000000
//a=control:trackID=1
//a=rtpmap:96 H264/90000
//a=fmtp:96 packetization-mode=1; profile-level-id=4D4029; sprop-parameter-sets=Z01AKZpmBkCb8uAtQEBAQXpw,aO48gA==
//m=audio 0 RTP/AVP 97
//c=IN IP4 0.0.0.0
//b=AS:32
//a=control:trackID=2
//a=rtpmap:97 G726-32/8000
// v=0
// o=- 14190294250618174561 14190294250618174561 IN IP4 127.0.0.1
// s=IP Webcam
// c=IN IP4 0.0.0.0
// t=0 0
// a=range:npt=now-
// a=control:*
// m=video 0 RTP/AVP 96
// a=rtpmap:96 H264/90000
// a=control:h264
// a=fmtp:96 packetization-mode=1;profile-level-id=42C028;sprop-parameter-sets=Z0LAKIyNQDwBEvLAPCIRqA==,aM48gA==;
// a=cliprect:0,0,1920,1080
// a=framerate:30.0
// a=framesize:96 1080-1920
// Pair first - name, e.g. "a"; second - value, e.g "cliprect:0,0,1920,1080"
@NonNull
private static List<Pair<String, String>> getDescribeParams(@NonNull String text) {
ArrayList<Pair<String, String>> list = new ArrayList<>();
String[] params = TextUtils.split(text, "\r\n");
for (String param : params) {
int i = param.indexOf('=');
if (i > 0) {
String name = param.substring(0, i).trim();
String value = param.substring(i + 1);
list.add(Pair.create(name, value));
}
}
return list;
}
@NonNull
private static SdpInfo getSdpInfoFromDescribeParams(@NonNull List<Pair<String, String>> params) {
SdpInfo sdpInfo = new SdpInfo();
Track[] tracks = getTracksFromDescribeParams(params);
sdpInfo.videoTrack = ((VideoTrack)tracks[0]);
sdpInfo.audioTrack = ((AudioTrack)tracks[1]);
sdpInfo.applicationTrack = ((ApplicationTrack)tracks[2]);
for (Pair<String, String> param : params) {
switch (param.first) {
case "s" -> sdpInfo.sessionName = param.second;
case "i" -> sdpInfo.sessionDescription = param.second;
}
}
return sdpInfo;
}
// a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
@Nullable
private static List<Pair<String, String>> getSdpAParams(@NonNull Pair<String, String> param) {
if (param.first.equals("a") && param.second.startsWith("fmtp:") && param.second.length() > 8) { //
String value = param.second.substring(8).trim(); // fmtp can be '96' (2 chars) and '127' (3 chars)
String[] paramsA = TextUtils.split(value, ";");
// streamtype=5
// profile-level-id=1
// mode=AAC-hbr
ArrayList<Pair<String, String>> retParams = new ArrayList<>();
for (String paramA: paramsA) {
paramA = paramA.trim();
// sprop-parameter-sets=Z0LAKIyNQDwBEvLAPCIRqA==,aM48gA==
int i = paramA.indexOf("=");
if (i != -1)
retParams.add(
Pair.create(
paramA.substring(0, i),
paramA.substring(i + 1)));
}
return retParams;
} else {
Log.w(TAG, "Not a valid fmtp");
}
return null;
}
@NonNull
private static byte[] getNalUnitFromSprop(String nalBase64) {
byte[] nal = Base64.decode(nalBase64, Base64.NO_WRAP);
byte[] nalWithStart = new byte[nal.length + 4];
// Add 00 00 00 01 NAL unit header
nalWithStart[0] = 0;
nalWithStart[1] = 0;
nalWithStart[2] = 0;
nalWithStart[3] = 1;
System.arraycopy(nal, 0, nalWithStart, 4, nal.length);
return nalWithStart;
}
private static void updateVideoTrackFromDescribeParam(@NonNull VideoTrack videoTrack, @NonNull Pair<String, String> param) {
// a=fmtp:96 packetization-mode=1;profile-level-id=42C028;sprop-parameter-sets=Z0LAKIyNQDwBEvLAPCIRqA==,aM48gA==;
// a=fmtp:96 packetization-mode=1; profile-level-id=4D4029; sprop-parameter-sets=Z01AKZpmBkCb8uAtQEBAQXpw,aO48gA==
// a=fmtp:99 sprop-parameter-sets=Z0LgKdoBQBbpuAgIMBA=,aM4ySA==;packetization-mode=1;profile-level-id=42e029
// a=fmtp:98 profile-id=1;sprop-sps=QgEBAWAAAAMAgAAAAwAAAwB4oAWCAJB/ja7tTd3Jdf+ACAAFtwUFBQQAAA+gAAGGoch3uUQD6AARlAB9AAIygg==;sprop-pps=RAHBcrAiQA==;sprop-vps=QAEMAf//AWAAAAMAgAAAAwAAAwB4rAk=
List<Pair<String, String>> params = getSdpAParams(param);
if (params != null) {
for (Pair<String, String> pair: params) {
switch (pair.first.toLowerCase()) {
case "sprop-sps" -> {
videoTrack.sps = getNalUnitFromSprop(pair.second);
}
case "sprop-pps" -> {
videoTrack.pps = getNalUnitFromSprop(pair.second);
}
case "sprop-vps" -> {
videoTrack.vps = getNalUnitFromSprop(pair.second);
}
case "sprop-parameter-sets" -> {
String[] paramsSpsPps = TextUtils.split(pair.second, ",");
if (paramsSpsPps.length > 1) {
videoTrack.sps = getNalUnitFromSprop(paramsSpsPps[0]);
videoTrack.pps = getNalUnitFromSprop(paramsSpsPps[1]);
// Base64.decode(paramsSpsPps[0], Base64.NO_WRAP);
// byte[] pps = Base64.decode(paramsSpsPps[1], Base64.NO_WRAP);
// byte[] nalSps = new byte[sps.length + 4];
// byte[] nalPps = new byte[pps.length + 4];
// // Add 00 00 00 01 NAL unit header
// nalSps[0] = 0;
// nalSps[1] = 0;
// nalSps[2] = 0;
// nalSps[3] = 1;
// System.arraycopy(sps, 0, nalSps, 4, sps.length);
// nalPps[0] = 0;
// nalPps[1] = 0;
// nalPps[2] = 0;
// nalPps[3] = 1;
// System.arraycopy(pps, 0, nalPps, 4, pps.length);
// videoTrack.sps = nalSps;
// videoTrack.pps = nalPps;
}
}
// packetization-mode=1
case "packetization-mode" -> {
// 0 - single NAL unit (default)
// 1 - non-interleaved mode (STAP-A and FU-A NAL units)
// 2 - interleaved mode
try {
int mode = Integer.parseInt(pair.second);
if (mode == 2)
Log.e(TAG, "Interleaved packetization mode is not supported");
} catch (NumberFormatException ignored) {
}
}
}
}
}
}
@NonNull
private static byte[] getBytesFromHexString(@NonNull String config) {
// "1210fff1" -> [12, 10, ff, f1]
return new BigInteger(config ,16).toByteArray();
}
private static void updateAudioTrackFromDescribeParam(@NonNull AudioTrack audioTrack, @NonNull Pair<String, String> param) {
// a=fmtp:96 streamtype=5; profile-level-id=14; mode=AAC-lbr; config=1388; sizeLength=6; indexLength=2; indexDeltaLength=2; constantDuration=1024; maxDisplacement=5
// a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
// a=fmtp:96 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1210fff15081ffdffc
List<Pair<String, String>> params = getSdpAParams(param);
if (params != null) {
for (Pair<String, String> pair: params) {
switch (pair.first.toLowerCase()) {
case "mode" -> audioTrack.mode = pair.second;
case "config" -> audioTrack.config = getBytesFromHexString(pair.second);
}
}
}
}
/**
* Search for header "Content-Base: rtsp://example.com/stream/"
* and return "rtsp://example.com/stream/"
*/
@Nullable
private static String getHeaderContentBase(@NonNull ArrayList<Pair<String, String>> headers) {
String contentBase = getHeader(headers, "content-base");
if (!TextUtils.isEmpty(contentBase)) {
return contentBase;
}
return null;
}
private static int getHeaderContentLength(@NonNull ArrayList<Pair<String, String>> headers) {
String length = getHeader(headers, "content-length");
if (!TextUtils.isEmpty(length)) {
try {
return Integer.parseInt(length);
} catch (NumberFormatException ignored) {
}
}
return -1;
}
private static int getSupportedCapabilities(@NonNull ArrayList<Pair<String, String>> headers) {
for (Pair<String, String> head: headers) {
String h = head.first.toLowerCase();
// Public: OPTIONS, DESCRIBE, SETUP, PLAY, GET_PARAMETER, SET_PARAMETER, TEARDOWN
if ("public".equals(h)) {
int mask = 0;
String[] tokens = TextUtils.split(head.second.toLowerCase(), ",");
for (String token: tokens) {
switch (token.trim()) {
case "options" -> mask |= RTSP_CAPABILITY_OPTIONS;
case "describe" -> mask |= RTSP_CAPABILITY_DESCRIBE;
case "announce" -> mask |= RTSP_CAPABILITY_ANNOUNCE;
case "setup" -> mask |= RTSP_CAPABILITY_SETUP;
case "play" -> mask |= RTSP_CAPABILITY_PLAY;
case "record" -> mask |= RTSP_CAPABILITY_RECORD;
case "pause" -> mask |= RTSP_CAPABILITY_PAUSE;
case "teardown" -> mask |= RTSP_CAPABILITY_TEARDOWN;
case "set_parameter" -> mask |= RTSP_CAPABILITY_SET_PARAMETER;
case "get_parameter" -> mask |= RTSP_CAPABILITY_GET_PARAMETER;
case "redirect" -> mask |= RTSP_CAPABILITY_REDIRECT;
}
}
return mask;
}
}
return RTSP_CAPABILITY_NONE;
}
@Nullable
private static Pair<String, String> getHeaderWwwAuthenticateDigestRealmAndNonce(@NonNull ArrayList<Pair<String, String>> headers) {
for (Pair<String, String> head: headers) {
String h = head.first.toLowerCase();
// WWW-Authenticate: Digest realm="AXIS_00408CEF081C", nonce="00054cecY7165349339ae05f7017797d6b0aaad38f6ff45", stale=FALSE
// WWW-Authenticate: Basic realm="AXIS_00408CEF081C"
// WWW-Authenticate: Digest realm="Login to 4K049EBPAG1D7E7", nonce="de4ccb15804565dc8a4fa5b115695f4f"
if ("www-authenticate".equals(h) && head.second.toLowerCase().startsWith("digest")) {
String v = head.second.substring(7).trim();
int begin, end;
begin = v.indexOf("realm=");
begin = v.indexOf('"', begin) + 1;
end = v.indexOf('"', begin);
String digestRealm = v.substring(begin, end);
begin = v.indexOf("nonce=");
begin = v.indexOf('"', begin)+1;
end = v.indexOf('"', begin);
String digestNonce = v.substring(begin, end);
return Pair.create(digestRealm, digestNonce);
}
}
return null;
}
@Nullable
private static String getHeaderWwwAuthenticateBasicRealm(@NonNull ArrayList<Pair<String, String>> headers) {
for (Pair<String, String> head: headers) {
// Session: ODgyODg3MjQ1MDczODk3NDk4Nw
String h = head.first.toLowerCase();
String v = head.second.toLowerCase();
// WWW-Authenticate: Digest realm="AXIS_00408CEF081C", nonce="00054cecY7165349339ae05f7017797d6b0aaad38f6ff45", stale=FALSE
// WWW-Authenticate: Basic realm="AXIS_00408CEF081C"
if ("www-authenticate".equals(h) && v.startsWith("basic")) {
v = v.substring(6).trim();
// realm=
// AXIS_00408CEF081C
String[] tokens = TextUtils.split(v, "\"");
if (tokens.length > 2)
return tokens[1];
}
}
return null;
}
// Basic authentication
@NonNull
private static String getBasicAuthHeader(@Nullable String username, @Nullable String password) {
String auth = (username == null ? "" : username) + ":" + (password == null ? "" : password);
return "Basic " + new String(Base64.encode(auth.getBytes(StandardCharsets.ISO_8859_1), Base64.NO_WRAP));
}
// Digest authentication
@Nullable
private static String getDigestAuthHeader(
@Nullable String username,
@Nullable String password,
@NonNull String method,
@NonNull String digestUri,
@NonNull String realm,
@NonNull String nonce) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] ha1;
if (username == null)
username = "";
if (password == null)
password = "";
// calc A1 digest
md.update(username.getBytes(StandardCharsets.ISO_8859_1));
md.update((byte) ':');
md.update(realm.getBytes(StandardCharsets.ISO_8859_1));
md.update((byte) ':');
md.update(password.getBytes(StandardCharsets.ISO_8859_1));
ha1 = md.digest();
// calc A2 digest
md.reset();
md.update(method.getBytes(StandardCharsets.ISO_8859_1));
md.update((byte) ':');
md.update(digestUri.getBytes(StandardCharsets.ISO_8859_1));
byte[] ha2 = md.digest();
// calc response
md.update(getHexStringFromBytes(ha1).getBytes(StandardCharsets.ISO_8859_1));
md.update((byte) ':');
md.update(nonce.getBytes(StandardCharsets.ISO_8859_1));
md.update((byte) ':');
// TODO add support for more secure version of digest auth
//md.update(nc.getBytes(StandardCharsets.ISO_8859_1));
//md.update((byte) ':');
//md.update(cnonce.getBytes(StandardCharsets.ISO_8859_1));
//md.update((byte) ':');
//md.update(qop.getBytes(StandardCharsets.ISO_8859_1));
//md.update((byte) ':');
md.update(getHexStringFromBytes(ha2).getBytes(StandardCharsets.ISO_8859_1));
String response = getHexStringFromBytes(md.digest());
// log.trace("username=\"{}\", realm=\"{}\", nonce=\"{}\", uri=\"{}\", response=\"{}\"",
// userName, digestRealm, digestNonce, digestUri, response);
return "Digest username=\"" + username + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", uri=\"" + digestUri + "\", response=\"" + response + "\"";
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@NonNull
private static String getHexStringFromBytes(@NonNull byte[] bytes) {
StringBuilder buf = new StringBuilder();
for (byte b : bytes)
buf.append(String.format("%02x", b));
return buf.toString();
}
@NonNull
private static String readContentAsText(@NonNull InputStream inputStream, int length) throws IOException {
if (length <= 0)
return "";
byte[] b = new byte[length];
int read = readData(inputStream, b, 0, length);
return new String(b, 0, read);
}
// int memcmp ( const void * ptr1, const void * ptr2, size_t num );
public static boolean memcmp(
@NonNull byte[] source1,
int offsetSource1,
@NonNull byte[] source2,
int offsetSource2,
int num) {
if (source1.length - offsetSource1 < num)
return false;
if (source2.length - offsetSource2 < num)
return false;
for (int i = 0; i < num; i++) {
if (source1[offsetSource1 + i] != source2[offsetSource2 + i])
return false;
}
return true;
}
private static void shiftLeftArray(@NonNull byte[] array, int num) {
// ABCDEF -> BCDEF
if (num - 1 >= 0)
System.arraycopy(array, 1, array, 0, num - 1);
}
private boolean readUntilBytesFound(@NonNull InputStream inputStream, @NonNull byte[] array) throws IOException {
byte[] buffer = new byte[array.length];
// Fill in buffer
if (NetUtils.readData(inputStream, buffer, 0, buffer.length) != buffer.length)
return false; // EOF
while (!exitFlag.get()) {
// Check if buffer is the same one
if (memcmp(buffer, 0, array, 0, buffer.length)) {
return true;
}
// ABCDEF -> BCDEFF
shiftLeftArray(buffer, buffer.length);
// Read 1 byte into last buffer item
if (NetUtils.readData(inputStream, buffer, buffer.length - 1, 1) != 1) {
return false; // EOF
}
}
return false;
}
// private boolean readUntilByteFound(@NonNull InputStream inputStream, byte bt) throws IOException {
// byte[] buffer = new byte[1];
// int readBytes;
// while (!exitFlag.get()) {
// readBytes = inputStream.read(buffer, 0, 1);
// if (readBytes == -1) // EOF
// return false;
// if (readBytes == 1 && buffer[0] == bt) {
// return true;
// }
// }
// return false;
// }
@Nullable
private String readLine(@NonNull InputStream inputStream) throws IOException {
byte[] bufferLine = new byte[MAX_LINE_SIZE];
int offset = 0;
int readBytes;
do {
// Didn't find "\r\n" within 4K bytes
if (offset >= MAX_LINE_SIZE) {
throw new NoResponseHeadersException();
}
// Read 1 byte
readBytes = inputStream.read(bufferLine, offset, 1);
if (readBytes == 1) {
// Check for EOL
// Some cameras like Linksys WVC200 do not send \n instead of \r\n
if (offset > 0 && /*bufferLine[offset-1] == '\r' &&*/ bufferLine[offset] == '\n') {
// Found empty EOL. End of header section
if (offset == 1)
return "";//break;
// Found EOL. Add to array.
return new String(bufferLine, 0, offset-1);
} else {
offset++;
}
}
} while (readBytes > 0 && !exitFlag.get());
return null;
}
private static int readData(@NonNull InputStream inputStream, @NonNull byte[] buffer, int offset, int length) throws IOException {
if (DEBUG) Log.v(TAG, "readData(offset=" + offset + ", length=" + length + ")");
int readBytes;
int totalReadBytes = 0;
do {
readBytes = inputStream.read(buffer, offset + totalReadBytes, length - totalReadBytes);
if (readBytes > 0)
totalReadBytes += readBytes;
} while (readBytes >= 0 && totalReadBytes < length);
return totalReadBytes;
}
private static void dumpHeaders(@NonNull ArrayList<Pair<String, String>> headers) {
if (DEBUG) {
for (Pair<String, String> head : headers) {
Log.d(TAG, head.first + ": " + head.second);
}
}
}
@Nullable
private static String getHeader(@NonNull ArrayList<Pair<String, String>> headers, @NonNull String header) {
for (Pair<String, String> head: headers) {
// Session: ODgyODg3MjQ1MDczODk3NDk4Nw
String h = head.first.toLowerCase();
if (header.toLowerCase().equals(h)) {
return head.second;
}
}
// Not found
return null;
}
public static class Builder {
private static final String DEFAULT_USER_AGENT = "Lavf58.29.100";
private final @NonNull Socket rtspSocket;
private final @NonNull String uriRtsp;
private final @NonNull AtomicBoolean exitFlag;
private final @NonNull RtspClientListener listener;
// private boolean sendOptionsCommand = true;
private boolean requestVideo = true;
private boolean requestAudio = true;
private boolean requestApplication = true;
private boolean debug = false;
private @Nullable String username = null;
private @Nullable String password = null;
private @Nullable String userAgent = DEFAULT_USER_AGENT;
public Builder(
@NonNull Socket rtspSocket,
@NonNull String uriRtsp,
@NonNull AtomicBoolean exitFlag,
@NonNull RtspClientListener listener) {
this.rtspSocket = rtspSocket;
this.uriRtsp = uriRtsp;
this.exitFlag = exitFlag;
this.listener = listener;
}
@NonNull
public Builder withDebug(boolean debug) {
this.debug = debug;
return this;
}
@NonNull
public Builder withCredentials(@Nullable String username, @Nullable String password) {
this.username = username;
this.password = password;
return this;
}
@NonNull
public Builder withUserAgent(@Nullable String userAgent) {
this.userAgent = userAgent;
return this;
}
// @NonNull
// public Builder sendOptionsCommand(boolean sendOptionsCommand) {
// this.sendOptionsCommand = sendOptionsCommand;
// return this;
// }
@NonNull
public Builder requestVideo(boolean requestVideo) {
this.requestVideo = requestVideo;
return this;
}
@NonNull
public Builder requestAudio(boolean requestAudio) {
this.requestAudio = requestAudio;
return this;
}
@NonNull
public Builder requestApplication(boolean requestApplication) {
this.requestApplication = requestApplication;
return this;
}
@NonNull
public RtspClient build() {
return new RtspClient(this);
}
}
}
class LoggerOutputStream extends BufferedOutputStream {
private boolean logging = true;
public LoggerOutputStream(@NonNull OutputStream out) {
super(out);
}
public synchronized void setLogging(boolean logging) {
this.logging = logging;
}
@Override
public synchronized void write(byte[] b, int off, int len) throws IOException {
super.write(b, off, len);
if (logging)
Log.i(RtspClient.TAG_DEBUG, new String(b, off, len));
}
}
================================================
FILE: library-client-rtsp/src/main/java/com/alexvas/rtsp/codec/AudioDecodeThread.kt
================================================
package com.alexvas.rtsp.codec
import android.media.*
import android.os.Process
import android.util.Log
import java.nio.ByteBuffer
class AudioDecodeThread (
private val mimeType: String,
private val sampleRate: Int,
private val channelCount: Int,
private val codecConfig: ByteArray?,
private val audioFrameQueue: AudioFrameQueue) : Thread() {
private var isRunning = true
fun stopAsync() {
if (DEBUG) Log.v(TAG, "stopAsync()")
isRunning = false
// Wake up sleep() code
interrupt()
}
override fun run() {
if (DEBUG) Log.d(TAG, "$name started")
Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO)
// Creating audio decoder
val decoder = MediaCodec.createDecoderByType(mimeType)
val format = MediaFormat.createAudioFormat(mimeType, sampleRate, channelCount)
if (mimeType == MediaFormat.MIMETYPE_AUDIO_AAC) {
val csd0 = codecConfig ?: getAacDecoderConfigData(MediaCodecInfo.CodecProfileLevel.AACObjectLC, sampleRate, channelCount)
format.setByteBuffer("csd-0", ByteBuffer.wrap(csd0))
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
} else if (mimeType == MediaFormat.MIMETYPE_AUDIO_OPUS) {
// TODO: Add Opus support
// val OPUS_IDENTIFICATION_HEADER = "OpusHead".toByteArray()
// val OPUS_PRE_SKIP_NSEC = ByteBuffer.allocate(8).putLong(11971).array()
// val OPUS_SEEK_PRE_ROLL_NSEC = ByteBuffer.allocate(8).putLong(80000000).array()
// val csd0 = ByteBuffer.allocate(8+1+1+2+4+2+1)
// csd0.put("OpusHead".toByteArray())
// // Version
// csd0.put(1)
// // Number of channels
// csd0.put(2)
// // Pre-skip
// csd0.putShort(0)
// csd0.putInt(sampleRate)
// // Output Gain
// csd0.putShort(0)
// // Channel Mapping Family
// csd0.put(0)
// Buffer buf = new Buffer();
// // Magic Signature:固定头,占8个字节,为字符串OpusHead
// buf.write("OpusHead".getBytes(StandardCharsets.UTF_8));
// // Version:版本号,占1字节,固定为0x01
// buf.writeByte(1);
// // Channel Count:通道数,占1字节,根据音频流通道自行设置,如0x02
// buf.writeByte(1);
// // Pre-skip:回放的时候从解码器中丢弃的samples数量,占2字节,为小端模式,默认设置0x00,
// buf.writeShortLe(0);
// // Input Sample Rate (Hz):音频流的Sample Rate,占4字节,为小端模式,根据实际情况自行设置
// buf.writeIntLe(currentFormat.HZ);
// //Output Gain:输出增益,占2字节,为小端模式,没有用到默认设置0x00, 0x00就好
// buf.writeShortLe(0);
// // Channel Mapping Family:通道映射系列,占1字节,默认设置0x00就好
// buf.writeByte(0);
// //Channel Mapping Table:可选参数,上面的Family默认设置0x00的时候可忽略
// format.setByteBuffer("csd-0", ByteBuffer.wrap(OPUS_IDENTIFICATION_HEADER).order(ByteOrder.BIG_ENDIAN))
// format.setByteBuffer("csd-1", ByteBuffer.wrap(OPUS_PRE_SKIP_NSEC).order(ByteOrder.BIG_ENDIAN))
// format.setByteBuffer("csd-2", ByteBuffer.wrap(OPUS_SEEK_PRE_ROLL_NSEC).order(ByteOrder.LITTLE_ENDIAN))
val csd0 = byteArrayOf(
0x4f, 0x70, 0x75, 0x73, // "Opus"
0x48, 0x65, 0x61, 0x64, // "Head"
0x01, // Version
0x02, // Channel Count
0x00, 0x00, // Pre skip
0x80.toByte(), 0xbb.toByte(), 0x00, 0x00, // Sample rate 48000
0x00, 0x00, // Output Gain (Q7.8 in dB)
0x00, // Mapping Family
)
val csd1 = byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
val csd2 = byteArrayOf(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
format.setByteBuffer("csd-0", ByteBuffer.wrap(csd0))
format.setByteBuffer("csd-1", ByteBuffer.wrap(csd1))
format.setByteBuffer("csd-2", ByteBuffer.wrap(csd2))
}
decoder.configure(format, null, null, 0)
decoder.start()
// Creating audio playback device
val outChannel = if (channelCount > 1) AudioFormat.CHANNEL_OUT_STEREO else AudioFormat.CHANNEL_OUT_MONO
val outAudio = AudioFormat.ENCODING_PCM_16BIT
val bufferSize = AudioTrack.getMinBufferSize(sampleRate, outChannel, outAudio)
// Log.i(TAG, "sampleRate: $sampleRate, bufferSize: $bufferSize".format(sampleRate, bufferSize))
val audioTrack = AudioTrack(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build(),
AudioFormat.Builder()
.setEncoding(outAudio)
.setChannelMask(outChannel)
.setSampleRate(sampleRate)
.build(),
bufferSize,
AudioTrack.MODE_STREAM,
0)
audioTrack.play()
val bufferInfo = MediaCodec.BufferInfo()
while (isRunning) {
val inIndex: Int = decoder.dequeueInputBuffer(10000L)
if (inIndex >= 0) {
// fill inputBuffers[inputBufferIndex] with valid data
var byteBuffer: ByteBuffer?
try {
byteBuffer = decoder.getInputBuffer(inIndex)
} catch (e: Exception) {
e.printStackTrace()
break
}
byteBuffer?.rewind()
// Preventing BufferOverflowException
// if (length > byteBuffer.limit()) throw DecoderFatalException("Error")
val audioFrame: FrameQueue.Frame?
try {
audioFrame = audioFrameQueue.pop()
if (audioFrame == null) {
Log.d(TAG, "Empty audio frame")
// Release input buffer
decoder.queueInputBuffer(inIndex, 0, 0, 0L, 0)
} else {
byteBuffer?.put(audioFrame.data, audioFrame.offset, audioFrame.length)
decoder.queueInputBuffer(inIndex, audioFrame.offset, audioFrame.length, audioFrame.timestampMs, 0)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
// Log.i(TAG, "inIndex: ${inIndex}")
try {
// Log.w(TAG, "outIndex: ${outIndex}")
if (!isRunning) break
when (val outIndex = decoder.dequeueOutputBuffer(bufferInfo, 10000L)) {
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> Log.d(TAG, "Decoder format changed: ${decoder.outputFormat}")
MediaCodec.INFO_TRY_AGAIN_LATER -> if (DEBUG) Log.d(TAG, "No output from decoder available")
else -> {
if (outIndex >= 0) {
val byteBuffer: ByteBuffer? = decoder.getOutputBuffer(outIndex)
val chunk = ByteArray(bufferInfo.size)
byteBuffer?.get(chunk)
byteBuffer?.clear()
if (chunk.isNotEmpty()) {
audioTrack.write(chunk, 0, chunk.size)
}
decoder.releaseOutputBuffer(outIndex, false)
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
// All decoded frames have been rendered, we can stop playing now
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
Log.d(TAG, "OutputBuffer BUFFER_FLAG_END_OF_STREAM")
break
}
}
audioTrack.flush()
audioTrack.release()
try {
decoder.stop()
decoder.release()
} catch (_: InterruptedException) {
} catch (e: Exception) {
e.printStackTrace()
}
audioFrameQueue.clear()
if (DEBUG) Log.d(TAG, "$name stopped")
}
companion object {
private val TAG: String = AudioDecodeThread::class.java.simpleName
private const val DEBUG = false
fun getAacDecoderConfigData(audioProfile: Int, sampleRate: Int, channels: Int): ByteArray {
// AOT_LC = 2
// 0001 0000 0000 0000
var extraDataAac = audioProfile shl 11
// Sample rate
when (sampleRate) {
7350 -> extraDataAac = extraDataAac or (0xC shl 7)
8000 -> extraDataAac = extraDataAac or (0xB shl 7)
11025 -> extraDataAac = extraDataAac or (0xA shl 7)
12000 -> extraDataAac = extraDataAac or (0x9 shl 7)
16000 -> extraDataAac = extraDataAac or (0x8 shl 7)
22050 -> extraDataAac = extraDataAac or (0x7 shl 7)
24000 -> extraDataAac = extraDataAac or (0x6 shl 7)
32000 -> extraDataAac = extraDataAac or (0x5 shl 7)
44100 -> extraDataAac = extraDataAac or (0x4 shl 7)
48000 -> extraDataAac = extraDataAac or (0x3 shl 7)
64000 -> extraDataAac = extraDataAac or (0x2 shl 7)
88200 -> extraDataAac = extraDataAac or (0x1 shl 7)
96000 -> extraDataAac = extraDataAac or (0x0 shl 7)
}
// Channels
extraDataAac = extraDataAac or (channels shl 3)
val extraData = ByteArray(2)
extraData[0] = (extraDataAac and 0xff00 shr 8).toByte() // high byte
extraData[1] = (extraDataAac and 0xff).toByte() // low byte
return extraData
}
}
}
================================================
FILE: library-client-rtsp/src/main/java/com/alexvas/rtsp/codec/FrameQueue.kt
================================================
package com.alexvas.rtsp.codec
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.TimeUnit
enum class VideoCodecType {
H264, H265, UNKNOWN
}
enum class AudioCodecType {
AAC_LC, G711_ALAW, G711_MLAW, UNKNOWN
}
class VideoFrameQueue(frameQueueCapacity: Int): FrameQueue<FrameQueue.VideoFrame>(frameQueueCapacity)
class AudioFrameQueue(frameQueueCapacity: Int): FrameQueue<FrameQueue.AudioFrame>(frameQueueCapacity)
/**
* Queue for concurrent adding/removing audio/video frames.
*/
open class FrameQueue<T>(private val frameQueueCapacity: Int) {
interface Frame {
val data: ByteArray
val offset: Int
val length: Int
val timestampMs: Long // presentation time in msec
}
data class VideoFrame(
/** Only H264 codec supported */
val codecType: VideoCodecType,
/** Indicates whether it is a keyframe or not */
val isKeyframe: Boolean,
override val data: ByteArray,
override val offset: Int,
override val length: Int,
/** Video frame timestamp (msec) generated by camera */
override val timestampMs: Long,
/** Captured (received) video frame timestamp (msec). If -1, not supported. */
val capturedTimestampMs: Long = -1
) : Frame
data class AudioFrame(
val codecType: AudioCodecType,
// val sampleRate: Int,
override val data: ByteArray,
override val offset: Int,
override val length: Int,
override val timestampMs: Long,
) : Frame
private val queue = ArrayBlockingQueue<T>(frameQueueCapacity)
val size: Int
get() = queue.size
val capacity: Int
get() = frameQueueCapacity
@Throws(InterruptedException::class)
fun push(frame: T): Boolean {
if (queue.offer(frame, 5, TimeUnit.MILLISECONDS)) {
return true
}
// Log.w(TAG, "Cannot add frame, queue is full")
return false
}
@Throws(InterruptedException::class)
open fun pop(timeout: Long = 1000): T? {
try {
val frame: T? = queue.poll(timeout, TimeUnit.MILLISECONDS)
// if (frame == null) {
// Log.w(TAG, "Cannot get frame within 1 sec, queue is empty")
// }
return frame
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
}
return null
}
fun clear() {
queue.clear()
}
fun copyInto(dstFrameQueue: FrameQueue<T>) {
dstFrameQueue.queue.addAll(queue)
}
companion object {
private val TAG: String = FrameQueue::class.java.simpleName
}
}
================================================
FILE: library-client-rtsp/src/main/java/com/alexvas/rtsp/codec/VideoDecodeThread.kt
================================================
package com.alexvas.rtsp.codec
import android.annotation.SuppressLint
import android.media.MediaCodec
import android.media.MediaCodec.OnFrameRenderedListener
import android.media.MediaFormat
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.Process
import android.util.Log
import com.alexvas.utils.MediaCodecUtils
import com.alexvas.utils.capabilitiesToString
import androidx.media3.common.util.Util
import com.alexvas.utils.VideoCodecUtils
import com.limelight.binding.video.MediaCodecHelper
import java.lang.Integer.min
import java.nio.ByteBuffer
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
abstract class VideoDecodeThread (
protected val mimeType: String,
protected val width: Int,
protected val height: Int,
protected val rotation: Int, // 0, 90, 180, 270
protected val videoFrameQueue: VideoFrameQueue,
protected val videoDecoderListener: VideoDecoderListener,
protected var videoDecoderType: DecoderType
) : Thread() {
enum class DecoderType {
HARDWARE,
SOFTWARE // fallback
}
interface VideoDecoderListener {
/** Video decoder successfully started */
fun onVideoDecoderStarted() {}
/** Video decoder successfully stopped */
fun onVideoDecoderStopped() {}
/** Fatal error occurred */
fun onVideoDecoderFailed(message: String?) {}
/** Resolution changed */
fun onVideoDecoderFormatChanged(width: Int, height: Int) {}
/** First video frame rendered */
fun onVideoDecoderFirstFrameRendered() {}
}
protected val uiHandler = Handler(Looper.getMainLooper())
protected var exitFlag = AtomicBoolean(false)
protected var firstFrameRendered = false
/** Decoder latency used for statistics */
@Volatile private var decoderLatency = -1
/** Flag for allowing calculating latency */
private var decoderLatencyRequested = false
/** Network latency used for statistics */
@Volatile private var networkLatency = -1
private var videoDecoderName: String? = null
private var firstFrameDecoded = false
@Volatile private var videoFrameRateStabilization = false
fun stopAsync() {
if (DEBUG) Log.v(TAG, "stopAsync()")
exitFlag.set(true)
// Wake up sleep() code
interrupt()
}
/**
* Currently used video decoder. Video decoder can be changed on runtime.
* If videoDecoderType set to HARDWARE, it can be switched to SOFTWARE in case of decoding issue
* (e.g. hardware decoder does not support the stream resolution).
* If videoDecoderType set to SOFTWARE, it will always remain SOFTWARE (no any changes).
*/
fun getCurrentVideoDecoderType(): DecoderType {
return videoDecoderType
}
fun getCurrentVideoDecoderName(): String? {
return videoDecoderName
}
/**
* Get frames de
gitextract_xa9itofy/ ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── alexvas/ │ │ └── rtsp/ │ │ └── demo/ │ │ ├── MainActivity.kt │ │ └── live/ │ │ ├── LiveFragment.kt │ │ ├── LiveViewModel.kt │ │ └── RawFragment.kt │ └── res/ │ ├── drawable/ │ │ ├── ic_camera_black_24dp.xml │ │ ├── ic_cctv_black_24dp.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ └── ic_text_subject_black_24dp.xml │ ├── layout/ │ │ ├── activity_main.xml │ │ ├── fragment_live.xml │ │ ├── fragment_logs.xml │ │ ├── fragment_raw.xml │ │ └── layout_rtsp_params.xml │ ├── menu/ │ │ └── bottom_nav_menu.xml │ ├── mipmap-anydpi-v26/ │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ ├── navigation/ │ │ └── mobile_navigation.xml │ └── values/ │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── library-client-rtsp/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.txt │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ ├── alexvas/ │ │ ├── rtsp/ │ │ │ ├── RtspClient.java │ │ │ ├── codec/ │ │ │ │ ├── AudioDecodeThread.kt │ │ │ │ ├── FrameQueue.kt │ │ │ │ ├── VideoDecodeThread.kt │ │ │ │ ├── VideoDecoderBitmapThread.kt │ │ │ │ ├── VideoDecoderSurfaceThread.kt │ │ │ │ └── color/ │ │ │ │ ├── ColorConverter.kt │ │ │ │ └── ColorConverterImage.kt │ │ │ ├── parser/ │ │ │ │ ├── AacParser.java │ │ │ │ ├── AudioParser.kt │ │ │ │ ├── G711Parser.kt │ │ │ │ ├── RtpH264Parser.kt │ │ │ │ ├── RtpH265Parser.kt │ │ │ │ ├── RtpHeaderParser.java │ │ │ │ └── RtpParser.kt │ │ │ └── widget/ │ │ │ ├── RtspImageView.kt │ │ │ ├── RtspListeners.kt │ │ │ ├── RtspProcessor.kt │ │ │ └── RtspSurfaceView.kt │ │ └── utils/ │ │ ├── ByteUtils.java │ │ ├── MediaCodecUtils.kt │ │ ├── NetUtils.java │ │ └── VideoCodecUtils.kt │ └── limelight/ │ └── binding/ │ └── video/ │ └── MediaCodecHelper.java └── settings.gradle
SYMBOL INDEX (141 symbols across 6 files)
FILE: library-client-rtsp/src/main/java/com/alexvas/rtsp/RtspClient.java
class RtspClient (line 129) | public class RtspClient {
method hasCapability (line 149) | public static boolean hasCapability(int capability, int capabilitiesMa...
type RtspClientListener (line 153) | public interface RtspClientListener {
method onRtspConnecting (line 154) | void onRtspConnecting();
method onRtspConnected (line 155) | void onRtspConnected(@NonNull SdpInfo sdpInfo);
method onRtspVideoNalUnitReceived (line 156) | void onRtspVideoNalUnitReceived(@NonNull byte[] data, int offset, in...
method onRtspAudioSampleReceived (line 157) | void onRtspAudioSampleReceived(@NonNull byte[] data, int offset, int...
method onRtspApplicationDataReceived (line 158) | void onRtspApplicationDataReceived(@NonNull byte[] data, int offset,...
method onRtspDisconnecting (line 159) | void onRtspDisconnecting();
method onRtspDisconnected (line 160) | void onRtspDisconnected();
method onRtspFailedUnauthorized (line 161) | void onRtspFailedUnauthorized();
method onRtspFailed (line 162) | void onRtspFailed(@Nullable String message);
type RtspClientKeepAliveListener (line 165) | private interface RtspClientKeepAliveListener {
method onRtspKeepAliveRequested (line 166) | void onRtspKeepAliveRequested();
class SdpInfo (line 169) | public static class SdpInfo {
class Track (line 185) | public abstract static class Track {
method toString (line 189) | @NonNull
class VideoTrack (line 199) | public static class VideoTrack extends Track {
method getAudioCodecName (line 212) | @NonNull
class AudioTrack (line 223) | public static class AudioTrack extends Track {
class ApplicationTrack (line 231) | public static class ApplicationTrack extends Track {
class UnauthorizedException (line 239) | private static class UnauthorizedException extends IOException {
method UnauthorizedException (line 240) | UnauthorizedException() {
class NoResponseHeadersException (line 245) | private final static class NoResponseHeadersException extends IOExcept...
method RtspClient (line 264) | private RtspClient(@NonNull RtspClient.Builder builder) {
method execute (line 279) | public void execute() {
method getUriForSetup (line 591) | @Nullable
method checkExitFlag (line 615) | private static void checkExitFlag(@NonNull AtomicBoolean exitFlag) thr...
method checkStatusCode (line 620) | private static void checkStatusCode(int code) throws IOException {
method readRtpData (line 631) | private static void readRtpData(
method sendSimpleCommand (line 786) | private static void sendSimpleCommand(
method sendOptionsCommand (line 807) | private static void sendOptionsCommand(
method sendGetParameterCommand (line 818) | private static void sendGetParameterCommand(
method sendDescribeCommand (line 830) | private static void sendDescribeCommand(
method sendTeardownCommand (line 849) | private static void sendTeardownCommand(
method sendSetupCommand (line 870) | private static void sendSetupCommand(
method sendPlayCommand (line 893) | private static void sendPlayCommand(
method readResponseStatusCode (line 914) | private int readResponseStatusCode(@NonNull InputStream inputStream) t...
method readResponseHeaders (line 944) | @NonNull
method getTracksFromDescribeParams (line 967) | @NonNull
method getDescribeParams (line 1144) | @NonNull
method getSdpInfoFromDescribeParams (line 1159) | @NonNull
method getSdpAParams (line 1178) | @Nullable
method getNalUnitFromSprop (line 1204) | @NonNull
method updateVideoTrackFromDescribeParam (line 1217) | private static void updateVideoTrackFromDescribeParam(@NonNull VideoTr...
method getBytesFromHexString (line 1276) | @NonNull
method updateAudioTrackFromDescribeParam (line 1282) | private static void updateAudioTrackFromDescribeParam(@NonNull AudioTr...
method getHeaderContentBase (line 1301) | @Nullable
method getHeaderContentLength (line 1310) | private static int getHeaderContentLength(@NonNull ArrayList<Pair<Stri...
method getSupportedCapabilities (line 1321) | private static int getSupportedCapabilities(@NonNull ArrayList<Pair<St...
method getHeaderWwwAuthenticateDigestRealmAndNonce (line 1349) | @Nullable
method getHeaderWwwAuthenticateBasicRealm (line 1376) | @Nullable
method getBasicAuthHeader (line 1397) | @NonNull
method getDigestAuthHeader (line 1404) | @Nullable
method getHexStringFromBytes (line 1461) | @NonNull
method readContentAsText (line 1469) | @NonNull
method memcmp (line 1479) | public static boolean memcmp(
method shiftLeftArray (line 1497) | private static void shiftLeftArray(@NonNull byte[] array, int num) {
method readUntilBytesFound (line 1503) | private boolean readUntilBytesFound(@NonNull InputStream inputStream, ...
method readLine (line 1539) | @Nullable
method readData (line 1570) | private static int readData(@NonNull InputStream inputStream, @NonNull...
method dumpHeaders (line 1582) | private static void dumpHeaders(@NonNull ArrayList<Pair<String, String...
method getHeader (line 1590) | @Nullable
class Builder (line 1603) | public static class Builder {
method Builder (line 1620) | public Builder(
method withDebug (line 1631) | @NonNull
method withCredentials (line 1637) | @NonNull
method withUserAgent (line 1644) | @NonNull
method requestVideo (line 1656) | @NonNull
method requestAudio (line 1662) | @NonNull
method requestApplication (line 1668) | @NonNull
method build (line 1674) | @NonNull
class LoggerOutputStream (line 1681) | class LoggerOutputStream extends BufferedOutputStream {
method LoggerOutputStream (line 1684) | public LoggerOutputStream(@NonNull OutputStream out) {
method setLogging (line 1688) | public synchronized void setLogging(boolean logging) {
method write (line 1692) | @Override
FILE: library-client-rtsp/src/main/java/com/alexvas/rtsp/parser/AacParser.java
class AacParser (line 19) | @SuppressLint("UnsafeOptInUsageError")
method AacParser (line 43) | public AacParser(@NonNull String aacMode) {
method processRtpPacketAndGetSample (line 50) | @Override
method handleSingleAacFrame (line 108) | private byte[] handleSingleAacFrame(ParsableByteArray packet) {
FILE: library-client-rtsp/src/main/java/com/alexvas/rtsp/parser/RtpHeaderParser.java
class RtpHeaderParser (line 13) | public class RtpHeaderParser {
class RtpHeader (line 20) | public static class RtpHeader {
method getTimestampMsec (line 32) | public long getTimestampMsec() {
method searchForNextRtpHeader (line 37) | private static boolean searchForNextRtpHeader(@NonNull InputStream i...
method parseData (line 70) | @Nullable
method getPacketSize (line 93) | private static int getPacketSize(@NonNull byte[] header) {
method dumpHeader (line 100) | public void dumpHeader() {
method readHeader (line 114) | @Nullable
FILE: library-client-rtsp/src/main/java/com/alexvas/utils/ByteUtils.java
class ByteUtils (line 8) | public class ByteUtils {
method memcmp (line 11) | public static boolean memcmp(
method copy (line 29) | public static byte[] copy(@NonNull byte[] src) {
FILE: library-client-rtsp/src/main/java/com/alexvas/utils/NetUtils.java
class NetUtils (line 24) | public class NetUtils {
class FakeX509TrustManager (line 30) | public static final class FakeX509TrustManager implements X509TrustMan...
method FakeX509TrustManager (line 40) | public FakeX509TrustManager() {
method checkClientTrusted (line 46) | public void checkClientTrusted(X509Certificate[] certificates, Strin...
method checkServerTrusted (line 53) | public void checkServerTrusted(X509Certificate[] certificates, Strin...
method checkServerTrusted (line 59) | @SuppressWarnings("unused")
method getAcceptedIssuers (line 67) | public X509Certificate[] getAcceptedIssuers() {
method createSslSocketAndConnect (line 72) | @NonNull
method createSocketAndConnect (line 94) | @NonNull
method createSocket (line 105) | @NonNull
method closeSocket (line 113) | public static void closeSocket(@Nullable Socket socket) throws IOExcep...
method readResponseHeaders (line 129) | @NonNull
method readLine (line 148) | @Nullable
method getResponseStatusCode (line 180) | public static int getResponseStatusCode(@NonNull ArrayList<String> hea...
method readContentAsText (line 216) | @NonNull
method readData (line 226) | public static int readData(@NonNull InputStream inputStream, @NonNull ...
FILE: library-client-rtsp/src/main/java/com/limelight/binding/video/MediaCodecHelper.java
class MediaCodecHelper (line 27) | public class MediaCodecHelper {
method isPowerVR (line 255) | private static boolean isPowerVR(String glRenderer) {
method getAdrenoVersionString (line 259) | private static String getAdrenoVersionString(String glRenderer) {
method isLowEndSnapdragonRenderer (line 278) | private static boolean isLowEndSnapdragonRenderer(String glRenderer) {
method getAdrenoRendererModelNumber (line 289) | private static int getAdrenoRendererModelNumber(String glRenderer) {
method isGLES31SnapdragonRenderer (line 303) | private static boolean isGLES31SnapdragonRenderer(String glRenderer) {
method initialize (line 308) | public static void initialize(Context context, String glRenderer) {
method isDecoderInList (line 419) | private static boolean isDecoderInList(List<String> decoderList, Strin...
method decoderSupportsAndroidRLowLatency (line 436) | private static boolean decoderSupportsAndroidRLowLatency(MediaCodecInf...
method decoderSupportsKnownVendorLowLatencyOption (line 452) | private static boolean decoderSupportsKnownVendorLowLatencyOption(Stri...
method decoderSupportsMaxOperatingRate (line 481) | private static boolean decoderSupportsMaxOperatingRate(String decoderN...
method setDecoderLowLatencyOptions (line 496) | public static boolean setDecoderLowLatencyOptions(MediaFormat videoFor...
method decoderSupportsFusedIdrFrame (line 598) | public static boolean decoderSupportsFusedIdrFrame(MediaCodecInfo deco...
method decoderSupportsAdaptivePlayback (line 614) | public static boolean decoderSupportsAdaptivePlayback(MediaCodecInfo d...
method decoderNeedsConstrainedHighProfile (line 636) | public static boolean decoderNeedsConstrainedHighProfile(String decode...
method decoderCanDirectSubmit (line 640) | public static boolean decoderCanDirectSubmit(String decoderName) {
method decoderNeedsSpsBitstreamRestrictions (line 644) | public static boolean decoderNeedsSpsBitstreamRestrictions(String deco...
method decoderNeedsBaselineSpsHack (line 648) | public static boolean decoderNeedsBaselineSpsHack(String decoderName) {
method getDecoderOptimalSlicesPerFrame (line 652) | public static byte getDecoderOptimalSlicesPerFrame(String decoderName) {
method decoderSupportsRefFrameInvalidationAvc (line 663) | public static boolean decoderSupportsRefFrameInvalidationAvc(String de...
method decoderSupportsRefFrameInvalidationHevc (line 678) | public static boolean decoderSupportsRefFrameInvalidationHevc(MediaCod...
method decoderSupportsRefFrameInvalidationAv1 (line 695) | public static boolean decoderSupportsRefFrameInvalidationAv1(MediaCode...
method decoderIsWhitelistedForHevc (line 706) | public static boolean decoderIsWhitelistedForHevc(MediaCodecInfo decod...
method isDecoderWhitelistedForAv1 (line 745) | public static boolean isDecoderWhitelistedForAv1(MediaCodecInfo decode...
method getMediaCodecList (line 771) | @SuppressWarnings("deprecation")
method dumpDecoders (line 782) | @SuppressWarnings("RedundantThrows")
method findPreferredDecoder (line 804) | private static MediaCodecInfo findPreferredDecoder() {
method isCodecBlacklisted (line 831) | private static boolean isCodecBlacklisted(MediaCodecInfo codecInfo) {
method findFirstDecoder (line 849) | public static MediaCodecInfo findFirstDecoder(String mimeType) {
method findProbableSafeDecoder (line 880) | public static MediaCodecInfo findProbableSafeDecoder(String mimeType, ...
method findKnownSafeDecoder (line 902) | @SuppressWarnings("RedundantThrows")
method readCpuinfo (line 963) | public static String readCpuinfo() throws Exception {
method stringContainsIgnoreCase (line 977) | private static boolean stringContainsIgnoreCase(String string, String ...
method isExynos4Device (line 981) | public static boolean isExynos4Device() {
Condensed preview — 66 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (382K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 22,
"preview": "github: alexeyvasilyev"
},
{
"path": ".gitignore",
"chars": 56,
"preview": "*.iml\n.gradle\n/local.properties\n/.idea\n/build\n.DS_Store\n"
},
{
"path": "LICENSE",
"chars": 11357,
"preview": " Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 6343,
"preview": "# rtsp-client-android\n<b>Lightweight RTSP client library for Android</b> with almost zero lag video decoding (achieved 2"
},
{
"path": "app/.gitignore",
"chars": 2333,
"preview": "# Created by https://www.gitignore.io/api/android,java,intellij\r\n\r\n### Android ###\r\n# Built application files\r\n*.apk\r\n*."
},
{
"path": "app/build.gradle",
"chars": 1996,
"preview": "apply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\n\nandroid {\n\n compileSdkVersion 36\n\n default"
},
{
"path": "app/proguard-rules.pro",
"chars": 751,
"preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
},
{
"path": "app/src/main/AndroidManifest.xml",
"chars": 771,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n <appli"
},
{
"path": "app/src/main/java/com/alexvas/rtsp/demo/MainActivity.kt",
"chars": 1001,
"preview": "package com.alexvas.rtsp.demo\n\nimport android.os.Bundle\nimport com.google.android.material.bottomnavigation.BottomNaviga"
},
{
"path": "app/src/main/java/com/alexvas/rtsp/demo/live/LiveFragment.kt",
"chars": 18637,
"preview": "package com.alexvas.rtsp.demo.live\n\nimport android.annotation.SuppressLint\nimport android.graphics.Bitmap\nimport android"
},
{
"path": "app/src/main/java/com/alexvas/rtsp/demo/live/LiveViewModel.kt",
"chars": 4434,
"preview": "package com.alexvas.rtsp.demo.live\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android"
},
{
"path": "app/src/main/java/com/alexvas/rtsp/demo/live/RawFragment.kt",
"chars": 10679,
"preview": "package com.alexvas.rtsp.demo.live\n\nimport android.annotation.SuppressLint\nimport android.net.Uri\nimport android.os.Bund"
},
{
"path": "app/src/main/res/drawable/ic_camera_black_24dp.xml",
"chars": 472,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:height=\"24dp\"\n android:width=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/ic_cctv_black_24dp.xml",
"chars": 701,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:height=\"24dp\"\n android:width=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/ic_launcher_background.xml",
"chars": 5606,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:wi"
},
{
"path": "app/src/main/res/drawable/ic_launcher_foreground.xml",
"chars": 704,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:height=\"16dp\"\n android:width=\"16dp\"\n "
},
{
"path": "app/src/main/res/drawable/ic_text_subject_black_24dp.xml",
"chars": 351,
"preview": "<!-- drawable/text_subject.xml -->\r\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\r\n android:heig"
},
{
"path": "app/src/main/res/layout/activity_main.xml",
"chars": 1005,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout\n xmlns:android=\"http://schemas.android.com/apk/res/android\"\n "
},
{
"path": "app/src/main/res/layout/fragment_live.xml",
"chars": 11163,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView\n xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xm"
},
{
"path": "app/src/main/res/layout/fragment_logs.xml",
"chars": 506,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<HorizontalScrollView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"
},
{
"path": "app/src/main/res/layout/fragment_raw.xml",
"chars": 2060,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<ScrollView\n xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xm"
},
{
"path": "app/src/main/res/layout/layout_rtsp_params.xml",
"chars": 3405,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n xmlns:android=\"http://schemas.android.com/apk/res/android\"\n "
},
{
"path": "app/src/main/res/menu/bottom_nav_menu.xml",
"chars": 573,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n <item\n "
},
{
"path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
"chars": 272,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
"chars": 272,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "app/src/main/res/navigation/mobile_navigation.xml",
"chars": 948,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<navigation xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:"
},
{
"path": "app/src/main/res/values/colors.xml",
"chars": 208,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"colorPrimary\">#40747A</color>\n <color name=\"color"
},
{
"path": "app/src/main/res/values/dimens.xml",
"chars": 211,
"preview": "<resources>\n <!-- Default screen margins, per the Android Design guidelines. -->\n <dimen name=\"activity_horizontal"
},
{
"path": "app/src/main/res/values/strings.xml",
"chars": 202,
"preview": "<resources>\n <string name=\"app_name\">Rtsp demo</string>\n <string name=\"title_live\">Live</string>\n <string name="
},
{
"path": "app/src/main/res/values/styles.xml",
"chars": 376,
"preview": "<resources>\n\n <!-- Base application theme. -->\n <style name=\"AppTheme\" parent=\"Theme.Material3.DayNight\">\n "
},
{
"path": "build.gradle",
"chars": 599,
"preview": "buildscript {\n\n ext.kotlin_version = '2.2.21'\n ext.compile_sdk_version = 36\n ext.min_sdk_version = 24\n ext.target_sd"
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"chars": 253,
"preview": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributi"
},
{
"path": "gradle.properties",
"chars": 51,
"preview": "org.gradle.jvmargs=-Xmx1g\nandroid.useAndroidX=true\n"
},
{
"path": "gradlew",
"chars": 8729,
"preview": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Lice"
},
{
"path": "gradlew.bat",
"chars": 2966,
"preview": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (th"
},
{
"path": "jitpack.yml",
"chars": 89,
"preview": "jdk:\n - openjdk17\n\ninstall:\n - ./gradlew build :library-client-rtsp:publishToMavenLocal"
},
{
"path": "library-client-rtsp/.gitignore",
"chars": 2333,
"preview": "# Created by https://www.gitignore.io/api/android,java,intellij\r\n\r\n### Android ###\r\n# Built application files\r\n*.apk\r\n*."
},
{
"path": "library-client-rtsp/build.gradle",
"chars": 985,
"preview": "plugins {\n id 'com.android.library'\n id 'kotlin-android'\n id 'maven-publish'\n}\n\napply plugin: 'com.android.libr"
},
{
"path": "library-client-rtsp/proguard-rules.txt",
"chars": 19,
"preview": "# Proguard rules.\n\n"
},
{
"path": "library-client-rtsp/src/main/AndroidManifest.xml",
"chars": 153,
"preview": "<manifest\n xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <uses-permission android:name=\"android.per"
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/rtsp/RtspClient.java",
"chars": 74359,
"preview": "package com.alexvas.rtsp;\n\nimport android.text.TextUtils;\nimport android.util.Base64;\nimport android.util.Log;\nimport an"
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/rtsp/codec/AudioDecodeThread.kt",
"chars": 10021,
"preview": "package com.alexvas.rtsp.codec\n\nimport android.media.*\nimport android.os.Process\nimport android.util.Log\nimport java.nio"
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/rtsp/codec/FrameQueue.kt",
"chars": 2678,
"preview": "package com.alexvas.rtsp.codec\n\nimport java.util.concurrent.ArrayBlockingQueue\nimport java.util.concurrent.TimeUnit\n\nenu"
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/rtsp/codec/VideoDecodeThread.kt",
"chars": 27021,
"preview": "package com.alexvas.rtsp.codec\n\nimport android.annotation.SuppressLint\nimport android.media.MediaCodec\nimport android.me"
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/rtsp/codec/VideoDecoderBitmapThread.kt",
"chars": 3015,
"preview": "package com.alexvas.rtsp.codec\n\nimport android.graphics.Bitmap\nimport android.graphics.Matrix\nimport android.media.Media"
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/rtsp/codec/VideoDecoderSurfaceThread.kt",
"chars": 5919,
"preview": "package com.alexvas.rtsp.codec\n\nimport android.media.MediaCodec\nimport android.media.MediaFormat\nimport android.util.Log"
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/rtsp/codec/color/ColorConverter.kt",
"chars": 2573,
"preview": "package com.alexvas.rtsp.codec.color\n\nimport android.annotation.SuppressLint\nimport android.graphics.Bitmap\nimport andro"
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/rtsp/codec/color/ColorConverterImage.kt",
"chars": 278,
"preview": "package com.alexvas.rtsp.codec.color\n\nimport android.graphics.Bitmap\nimport android.media.Image\n\nabstract class ColorCon"
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/rtsp/parser/AacParser.java",
"chars": 6637,
"preview": "package com.alexvas.rtsp.parser;\n\nimport android.annotation.SuppressLint;\nimport android.util.Log;\n\nimport androidx.anno"
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/rtsp/parser/AudioParser.kt",
"chars": 173,
"preview": "package com.alexvas.rtsp.parser\n\nabstract class AudioParser {\n abstract fun processRtpPacketAndGetSample(\n dat"
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/rtsp/parser/G711Parser.kt",
"chars": 271,
"preview": "package com.alexvas.rtsp.parser\n\nclass G711Parser() : AudioParser() {\n override fun processRtpPacketAndGetSample(\n "
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/rtsp/parser/RtpH264Parser.kt",
"chars": 4868,
"preview": "package com.alexvas.rtsp.parser\n\nimport android.util.Log\nimport com.alexvas.utils.VideoCodecUtils\nimport com.alexvas.uti"
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/rtsp/parser/RtpH265Parser.kt",
"chars": 5304,
"preview": "package com.alexvas.rtsp.parser\n\nimport android.util.Log\n\nclass RtpH265Parser: RtpParser() {\n\n override fun processRt"
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/rtsp/parser/RtpHeaderParser.java",
"chars": 5736,
"preview": "package com.alexvas.rtsp.parser;\n\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotati"
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/rtsp/parser/RtpParser.kt",
"chars": 841,
"preview": "package com.alexvas.rtsp.parser\n\nabstract class RtpParser {\n\n abstract fun processRtpPacketAndGetNalUnit(data: ByteAr"
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/rtsp/widget/RtspImageView.kt",
"chars": 4375,
"preview": "package com.alexvas.rtsp.widget\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.net.Uri\nim"
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/rtsp/widget/RtspListeners.kt",
"chars": 862,
"preview": "package com.alexvas.rtsp.widget\n\n/**\n * Listener for getting RTSP status update.\n */\ninterface RtspStatusListener {\n "
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/rtsp/widget/RtspProcessor.kt",
"chars": 24939,
"preview": "package com.alexvas.rtsp.widget\n\nimport android.annotation.SuppressLint\nimport android.media.MediaFormat\nimport android."
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/rtsp/widget/RtspSurfaceView.kt",
"chars": 6523,
"preview": "package com.alexvas.rtsp.widget\n\nimport android.content.Context\nimport android.net.Uri\nimport android.util.AttributeSet\n"
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/utils/ByteUtils.java",
"chars": 927,
"preview": "package com.alexvas.utils;\n\nimport androidx.annotation.NonNull;\n\nimport java.io.File;\nimport java.io.FileOutputStream;\n\n"
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/utils/MediaCodecUtils.kt",
"chars": 3360,
"preview": "package com.alexvas.utils\n\nimport android.annotation.SuppressLint\nimport android.util.Log\nimport android.util.Range\nimpo"
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/utils/NetUtils.java",
"chars": 8958,
"preview": "package com.alexvas.utils;\n\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nul"
},
{
"path": "library-client-rtsp/src/main/java/com/alexvas/utils/VideoCodecUtils.kt",
"chars": 14687,
"preview": "package com.alexvas.utils\n\nimport android.annotation.SuppressLint\nimport android.util.Log\nimport androidx.media3.contain"
},
{
"path": "library-client-rtsp/src/main/java/com/limelight/binding/video/MediaCodecHelper.java",
"chars": 45505,
"preview": "package com.limelight.binding.video;\n\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.FileReader;\nimp"
},
{
"path": "settings.gradle",
"chars": 45,
"preview": "include ':library-client-rtsp'\ninclude ':app'"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the alexeyvasilyev/rtsp-client-android GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 66 files (354.9 KB), approximately 85.0k tokens, and a symbol index with 141 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.