├── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── pan
│ │ └── project
│ │ └── fastrtsplive
│ │ ├── Extensions.kt
│ │ ├── MainActivity.kt
│ │ └── RtspCameraXFragment.kt
│ └── res
│ ├── drawable
│ ├── ic_launcher_background.xml
│ └── ic_launcher_foreground.xml
│ ├── layout
│ ├── activity_main.xml
│ └── fragment_camera_preview.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-mdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── values-night
│ └── themes.xml
│ ├── values
│ ├── colors.xml
│ ├── strings.xml
│ └── themes.xml
│ └── xml
│ ├── backup_rules.xml
│ └── data_extraction_rules.xml
├── build.gradle.kts
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── rtspserver
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── pedro
│ │ └── rtspserver
│ │ ├── RtspServerCamera1.kt
│ │ ├── RtspServerCamera2.kt
│ │ ├── RtspServerDisplay.kt
│ │ ├── RtspServerFromFile.kt
│ │ ├── RtspServerOnlyAudio.kt
│ │ ├── RtspServerStream.kt
│ │ ├── server
│ │ ├── ClientListener.kt
│ │ ├── IpType.kt
│ │ ├── RtspServer.kt
│ │ ├── ServerClient.kt
│ │ ├── ServerCommandManager.kt
│ │ └── ServerListener.kt
│ │ └── util
│ │ └── RtspServerStreamClient.kt
│ └── res
│ └── values
│ └── strings.xml
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | .idea
4 | /local.properties
5 | /.idea/caches
6 | /.idea/libraries
7 | /.idea/modules.xml
8 | /.idea/workspace.xml
9 | /.idea/navEditor.xml
10 | /.idea/assetWizardSettings.xml
11 | .DS_Store
12 | /build
13 | /captures
14 | .externalNativeBuild
15 | .cxx
16 | local.properties
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### 此RTSP推流程序分两部分:
2 |
3 | 1. **采集和编码**:使用我编写的 [CameraX-H264](https://github.com/PanPersonalProject/CameraX-H264) 进行摄像头数据采集并编码为H264,同时采集麦克风数据并编码为AAC。
4 |
5 | 2. **RTSP服务器**:
6 |
7 | -使用 PedroSG94 的 [RTSP-Server](https://github.com/pedroSG94/RTSP-Server) 进行推流(main分支)
8 |
9 | -使用live555实现RTSP服务器,请查看[live555](https://github.com/PanPersonalProject/RtspAndroid/tree/live555)分支
10 |
11 | #### 推送H264流
12 |
13 | ```kotlin
14 | private val cameraPreviewInterface = object : CameraPreviewInterface {
15 | override fun getPreviewView(): PreviewView = binding.preview
16 |
17 | override fun onSpsPpsVps(sps: ByteBuffer, pps: ByteBuffer?, vps: ByteBuffer?) {
18 | val newSps = sps.duplicate()
19 | val newPps = pps?.duplicate()
20 | val newVps = vps?.duplicate() // H265需要vps
21 | rtspServer.setVideoInfo(newSps, newPps, newVps) // 设置SPS、PPS到SDP协议中
22 | if (!rtspServer.isRunning) {
23 | rtspServer.startServer()
24 | }
25 | }
26 |
27 | override fun onVideoBuffer(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) {
28 | rtspServer.sendVideo(h264Buffer, info) // 发送H264数据
29 | }
30 | }
31 | ```
32 |
33 | #### 推送AAC流
34 |
35 | ```kotlin
36 | private val aacInterface = object : AacInterface {
37 | override fun getAacData(aacBuffer: ByteBuffer, info: MediaCodec.BufferInfo) {
38 | rtspServer.sendAudio(aacBuffer, info) // 发送AAC数据
39 | }
40 |
41 | override fun onAudioFormat(mediaFormat: MediaFormat) {
42 | rtspServer.setAudioInfo(
43 | sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE),
44 | isStereo = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT) == 2
45 | )
46 | }
47 | }
48 | ```
49 |
50 | 完整流程请参考 [RtspCameraXFragment](app/src/main/java/pan/project/fastrtsplive/RtspCameraXFragment.kt)。
51 |
52 | #### RTSP server Log
53 | ```kotlin
54 | rtspServer.setLogs(needShowLog)// 是否打印所有日志
55 | ```
56 |
57 | tag:CommandsManager 可以看到rtsp协议交互信息,和推流url地址
58 |
59 |
60 | **Log示例**:
61 | ```log
62 | 2024-07-24 16:43:33.823 5905-8590 CommandsManager pan.project.fastrtsplive I OPTIONS rtsp://192.168.0.106:1935/ RTSP/1.0
63 | CSeq: 2
64 | User-Agent: LibVLC/3.0.20 (LIVE555 Streaming Media v2016.11.28)
65 | 2024-07-24 16:43:33.839 5905-8590 CommandsManager pan.project.fastrtsplive I DESCRIBE rtsp://192.168.0.106:1935/ RTSP/1.0
66 | CSeq: 3
67 | User-Agent: LibVLC/3.0.20 (LIVE555 Streaming Media v2016.11.28)
68 | Accept: application/sdp
69 | 2024-07-24 16:43:33.854 5905-8590 CommandsManager pan.project.fastrtsplive I SETUP rtsp://192.168.0.106:1935/streamid=0 RTSP/1.0
70 | CSeq: 4
71 | User-Agent: LibVLC/3.0.20 (LIVE555 Streaming Media v2016.11.28)
72 | Transport: RTP/AVP;unicast;client_port=51050-51051
73 | 2024-07-24 16:43:33.863 5905-8590 CommandsManager pan.project.fastrtsplive I SETUP rtsp://192.168.0.106:1935/streamid=1 RTSP/1.0
74 | CSeq: 5
75 | User-Agent: LibVLC/3.0.20 (LIVE555 Streaming Media v2016.11.28)
76 | Transport: RTP/AVP;unicast;client_port=51052-51053
77 | Session: 1185d20035702ca
78 | 2024-07-24 16:43:33.869 5905-8590 CommandsManager pan.project.fastrtsplive I PLAY rtsp://192.168.0.106:1935/ RTSP/1.0
79 | CSeq: 6
80 | User-Agent: LibVLC/3.0.20 (LIVE555 Streaming Media v2016.11.28)
81 | Session: 1185d20035702ca
82 | Range: npt=0.000-
83 | ```
84 |
85 |
86 | #### 开发环境:
87 |
88 | Android Studio Ladybug | 2024.1.3 Canary 1
89 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @Suppress("DSL_SCOPE_VIOLATION")
2 | plugins {
3 | alias(libs.plugins.androidApplication)
4 | alias(libs.plugins.jetbrainsKotlinAndroid)
5 | }
6 |
7 | android {
8 | namespace = "pan.project.fastrtsplive"
9 | compileSdk = 34
10 |
11 | defaultConfig {
12 | applicationId = "pan.project.fastrtsplive"
13 | minSdk = libs.versions.minSdk.get().toInt()
14 | targetSdk = 34
15 | versionCode = 1
16 | versionName = "1.0"
17 | ndk {
18 | abiFilters.add("arm64-v8a")
19 | }
20 |
21 | }
22 |
23 | buildTypes {
24 | release {
25 | isMinifyEnabled = false
26 | proguardFiles(
27 | getDefaultProguardFile("proguard-android-optimize.txt"),
28 | "proguard-rules.pro"
29 | )
30 | }
31 | }
32 |
33 | sourceSets {
34 | getByName("debug") {
35 | java.srcDirs(
36 | "src/main/java",
37 | "build/generated/data_binding_base_class_source_out/debug/out"
38 | )
39 | }
40 | }
41 |
42 | compileOptions {
43 | sourceCompatibility = JavaVersion.VERSION_1_8
44 | targetCompatibility = JavaVersion.VERSION_1_8
45 | }
46 | kotlinOptions {
47 | jvmTarget = "1.8"
48 | }
49 |
50 | buildFeatures {
51 | viewBinding = true
52 | }
53 | }
54 |
55 | dependencies {
56 | implementation(libs.bundles.essential)
57 | implementation(libs.bundles.camerax)
58 | implementation(libs.cameraRecord)
59 | // implementation(project(":camera_record"))
60 |
61 | implementation(libs.rootencoder.library)
62 | implementation(project(":rtspserver"))
63 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
11 |
12 |
13 |
14 |
24 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/java/pan/project/fastrtsplive/Extensions.kt:
--------------------------------------------------------------------------------
1 | package pan.project.fastrtsplive
2 |
3 | import android.app.Activity
4 | import android.widget.Toast
5 | import androidx.fragment.app.Fragment
6 |
7 |
8 | fun Activity.toast(message: String, duration: Int = Toast.LENGTH_SHORT) {
9 | Toast.makeText(this, message, duration).show()
10 | }
11 | fun Fragment.toast(message: String, duration: Int = Toast.LENGTH_SHORT) {
12 | Toast.makeText(context, message, duration).show()
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/pan/project/fastrtsplive/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package pan.project.fastrtsplive
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 |
6 | class MainActivity : AppCompatActivity() {
7 | override fun onCreate(savedInstanceState: Bundle?) {
8 | super.onCreate(savedInstanceState)
9 | setContentView(R.layout.activity_main)
10 | }
11 |
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/java/pan/project/fastrtsplive/RtspCameraXFragment.kt:
--------------------------------------------------------------------------------
1 | package pan.project.fastrtsplive
2 |
3 | import android.Manifest
4 | import android.content.pm.PackageManager
5 | import android.media.MediaCodec
6 | import android.media.MediaFormat
7 | import android.os.Bundle
8 | import android.util.Log
9 | import android.view.LayoutInflater
10 | import android.view.View
11 | import android.view.ViewGroup
12 | import androidx.activity.result.contract.ActivityResultContracts
13 | import androidx.appcompat.app.AlertDialog
14 | import androidx.camera.core.ImageProxy
15 | import androidx.camera.view.PreviewView
16 | import androidx.core.content.ContextCompat
17 | import androidx.fragment.app.Fragment
18 | import com.pedro.common.AudioCodec
19 | import com.pedro.common.ConnectChecker
20 | import com.pedro.common.VideoCodec
21 | import com.pedro.rtspserver.server.RtspServer
22 | import pan.lib.camera_record.media.StreamManager
23 | import pan.lib.camera_record.media.audio.AacInterface
24 | import pan.lib.camera_record.media.video.CameraPreviewInterface
25 | import pan.lib.camera_record.media.yuv.BitmapUtils
26 | import pan.project.fastrtsplive.databinding.FragmentCameraPreviewBinding
27 | import java.nio.ByteBuffer
28 |
29 | /**
30 | * @author pan qi
31 | * @since 2024/7/23
32 | * 使用 tag:CommandsManager 可以看到rtsp协议交互信息,和推流url
33 | */
34 | class RtspCameraXFragment : Fragment() {
35 |
36 | private lateinit var binding: FragmentCameraPreviewBinding
37 | private lateinit var streamManager: StreamManager
38 | private lateinit var rtspServer: RtspServer
39 |
40 | override fun onCreateView(
41 | inflater: LayoutInflater, container: ViewGroup?,
42 | savedInstanceState: Bundle?
43 | ): View {
44 | binding = FragmentCameraPreviewBinding.inflate(inflater, container, false)
45 | return binding.root
46 | }
47 |
48 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
49 | super.onViewCreated(view, savedInstanceState)
50 | streamManager = StreamManager(
51 | requireContext(),
52 | viewLifecycleOwner,
53 | binding.prewview,
54 | cameraPreviewInterface,
55 | aacInterface
56 | )
57 |
58 | binding.cameraSwitchButton.setOnClickListener {
59 | streamManager.switchCamera()
60 | }
61 |
62 | binding.stopButton.setOnClickListener {
63 | streamManager.stop()
64 | }
65 | initRtspServer()
66 |
67 | requestPermissions()
68 |
69 |
70 | }
71 |
72 |
73 | private fun initRtspServer() {
74 | rtspServer = RtspServer(object : ConnectChecker {
75 | override fun onAuthError() {
76 | toast("Auth error")
77 | }
78 |
79 | override fun onAuthSuccess() {
80 | toast("Auth success")
81 | }
82 |
83 | override fun onConnectionFailed(reason: String) {
84 | toast("Failed: $reason")
85 | }
86 |
87 | override fun onConnectionStarted(url: String) {
88 | toast("Connecting: $url")
89 | }
90 |
91 | override fun onConnectionSuccess() {
92 | toast("Connected")
93 | }
94 |
95 | override fun onDisconnect() {
96 | toast("Disconnected")
97 | }
98 |
99 | }, port = 1935)
100 | rtspServer.setVideoCodec(VideoCodec.H264)
101 | rtspServer.setAudioCodec(AudioCodec.AAC)
102 | }
103 |
104 | private val cameraPreviewInterface = object : CameraPreviewInterface {
105 | override fun getPreviewView(): PreviewView = binding.prewview
106 |
107 | override fun onNv21Frame(nv21: ByteArray, imageProxy: ImageProxy) {
108 | val bitmap = BitmapUtils.getBitmap(
109 | ByteBuffer.wrap(nv21),
110 | imageProxy.width,
111 | imageProxy.height,
112 | imageProxy.imageInfo.rotationDegrees
113 | )
114 | binding.myImageView.post {
115 | binding.myImageView.setImageBitmap(bitmap)
116 | }
117 | }
118 |
119 | override fun onSpsPpsVps(sps: ByteBuffer, pps: ByteBuffer?, vps: ByteBuffer?) {
120 | val newSps = sps.duplicate()
121 | val newPps = pps?.duplicate()
122 | val newVps = vps?.duplicate()
123 | rtspServer.setVideoInfo(newSps, newPps, newVps)
124 | if (!rtspServer.isRunning()) {
125 | rtspServer.startServer()
126 | }
127 | }
128 |
129 | override fun onVideoBuffer(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) {
130 | rtspServer.sendVideo(h264Buffer, info)
131 | // Log.d("RtspCameraXFragment", "onVideoBuffer: ${info.getFormattedPresentationTime()}")
132 | }
133 | }
134 |
135 | private val aacInterface = object : AacInterface {
136 | override fun getAacData(aacBuffer: ByteBuffer, info: MediaCodec.BufferInfo) {
137 | rtspServer.sendAudio(aacBuffer, info)
138 | // Log.d("RtspCameraXFragment", "onAudioBuffer: ${info.getFormattedPresentationTime()}")
139 | }
140 |
141 | override fun onAudioFormat(mediaFormat: MediaFormat) {
142 | Log.d("CameraXPreviewFragment", "onAudioFormat: $mediaFormat")
143 | rtspServer.setAudioInfo(sampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE), isStereo = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT) == 2)
144 |
145 | }
146 | }
147 |
148 | private val requestPermissionLauncher =
149 | registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
150 | handlePermissionsResult(permissions)
151 | }
152 |
153 | private fun requestPermissions() {
154 | // 分别检查相机和录音权限的状态
155 | val cameraPermissionStatus = ContextCompat.checkSelfPermission(
156 | requireContext(),
157 | Manifest.permission.CAMERA
158 | )
159 | val recordAudioPermissionStatus = ContextCompat.checkSelfPermission(
160 | requireContext(),
161 | Manifest.permission.RECORD_AUDIO
162 | )
163 |
164 | // 如果任一权限未被授予,则发起权限请求
165 | if (cameraPermissionStatus != PackageManager.PERMISSION_GRANTED ||
166 | recordAudioPermissionStatus != PackageManager.PERMISSION_GRANTED
167 | ) {
168 | requestPermissionLauncher.launch(
169 | arrayOf(
170 | Manifest.permission.CAMERA,
171 | Manifest.permission.RECORD_AUDIO
172 | )
173 | )
174 | } else {
175 | // 权限确认后,启动streamManager
176 | binding.root.post {
177 | streamManager.start()
178 | }
179 | }
180 | }
181 |
182 | private fun handlePermissionsResult(permissions: Map) {
183 | val allGranted = permissions.all { it.value }
184 | if (allGranted) {
185 | binding.root.post {
186 | streamManager.start()
187 | }
188 | } else {
189 | val deniedPermissions = permissions.filter { !it.value }.keys.joinToString("\n")
190 | showPermissionDeniedDialog(deniedPermissions)
191 | }
192 | }
193 |
194 | private fun showPermissionDeniedDialog(deniedPermissions: String) {
195 | AlertDialog.Builder(requireContext())
196 | .setTitle("以下权限被拒绝")
197 | .setMessage(deniedPermissions)
198 | .setPositiveButton("确定", null)
199 | .show()
200 | }
201 |
202 | override fun onDestroyView() {
203 | streamManager.stop()
204 | rtspServer.stopServer()
205 | super.onDestroyView()
206 | }
207 |
208 |
209 | }
210 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_camera_preview.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
24 |
31 |
32 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PanPersonalProject/RtspAndroid/fff625ffb8adf8844a2b6fc33d9b749d7f32aaeb/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PanPersonalProject/RtspAndroid/fff625ffb8adf8844a2b6fc33d9b749d7f32aaeb/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PanPersonalProject/RtspAndroid/fff625ffb8adf8844a2b6fc33d9b749d7f32aaeb/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PanPersonalProject/RtspAndroid/fff625ffb8adf8844a2b6fc33d9b749d7f32aaeb/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PanPersonalProject/RtspAndroid/fff625ffb8adf8844a2b6fc33d9b749d7f32aaeb/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PanPersonalProject/RtspAndroid/fff625ffb8adf8844a2b6fc33d9b749d7f32aaeb/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PanPersonalProject/RtspAndroid/fff625ffb8adf8844a2b6fc33d9b749d7f32aaeb/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PanPersonalProject/RtspAndroid/fff625ffb8adf8844a2b6fc33d9b749d7f32aaeb/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PanPersonalProject/RtspAndroid/fff625ffb8adf8844a2b6fc33d9b749d7f32aaeb/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PanPersonalProject/RtspAndroid/fff625ffb8adf8844a2b6fc33d9b749d7f32aaeb/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | FastRtspLive
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
3 | plugins {
4 | alias(libs.plugins.androidApplication) apply false
5 | alias(libs.plugins.jetbrainsKotlinAndroid) apply false
6 | alias(libs.plugins.androidLibrary) apply false
7 | }
8 | true // Needed to make the Suppress annotation work for the plugins block
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.5.1"
3 | cameraxVersion = "1.4.0-beta02"
4 | kotlin = "2.0.0"
5 | coreKtx = "1.13.1"
6 | coroutines = "1.8.1"
7 | appcompat = "1.7.0"
8 | material = "1.12.0"
9 | constraintlayout = "2.1.4"
10 | minSdk = "29"
11 | cameraRecord="1.2.0"
12 | rootencoder = "2.4.6"
13 |
14 | [libraries]
15 | androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraxVersion" }
16 | androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "cameraxVersion" }
17 | androidx-camera-extensions = { module = "androidx.camera:camera-extensions", version.ref = "cameraxVersion" }
18 | androidx-camera-video = { module = "androidx.camera:camera-video", version.ref = "cameraxVersion" }
19 | androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "cameraxVersion" }
20 | androidx-camera-view = { module = "androidx.camera:camera-view", version.ref = "cameraxVersion" }
21 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
22 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
23 | material = { group = "com.google.android.material", name = "material", version.ref = "material" }
24 | androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
25 | cameraRecord = { module = "com.github.PanPersonalProject:CameraX-H264", version.ref = "cameraRecord" }
26 | kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
27 | rootencoder-library = { module = "com.github.pedroSG94.RootEncoder:library", version.ref = "rootencoder" }
28 |
29 | [bundles]
30 | camerax = [
31 | "androidx-camera-camera2",
32 | "androidx-camera-core",
33 | "androidx-camera-extensions",
34 | "androidx-camera-video",
35 | "androidx-camera-lifecycle",
36 | "androidx-camera-view"
37 | ]
38 |
39 | essential = [
40 | "androidx-core-ktx",
41 | "androidx-appcompat",
42 | "material",
43 | "androidx-constraintlayout"
44 | ]
45 |
46 | [plugins]
47 | androidApplication = { id = "com.android.application", version.ref = "agp" }
48 | jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
49 | androidLibrary = { id = "com.android.library", version.ref = "agp" }
50 |
51 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PanPersonalProject/RtspAndroid/fff625ffb8adf8844a2b6fc33d9b749d7f32aaeb/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Feb 03 02:29:42 CST 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/rtspserver/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/rtspserver/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.androidLibrary)
3 | alias(libs.plugins.jetbrainsKotlinAndroid)
4 | }
5 | android {
6 | namespace = "com.pedro.rtspserver"
7 | compileSdk = 34
8 |
9 | defaultConfig {
10 | minSdk = 16
11 | lint.targetSdk = 34
12 | }
13 |
14 | buildTypes {
15 | release {
16 | isMinifyEnabled = false
17 | }
18 | }
19 |
20 | compileOptions {
21 | sourceCompatibility = JavaVersion.VERSION_17
22 | targetCompatibility = JavaVersion.VERSION_17
23 | }
24 | kotlinOptions {
25 | jvmTarget = "17"
26 | }
27 |
28 |
29 | }
30 |
31 |
32 |
33 | dependencies {
34 | implementation(libs.kotlinx.coroutines.android)
35 | implementation(libs.rootencoder.library)
36 | }
37 |
--------------------------------------------------------------------------------
/rtspserver/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/rtspserver/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/rtspserver/src/main/java/com/pedro/rtspserver/RtspServerCamera1.kt:
--------------------------------------------------------------------------------
1 | package com.pedro.rtspserver
2 |
3 | import android.content.Context
4 | import android.media.MediaCodec
5 | import android.os.Build
6 | import android.view.SurfaceView
7 | import android.view.TextureView
8 | import androidx.annotation.RequiresApi
9 | import com.pedro.common.AudioCodec
10 | import com.pedro.common.ConnectChecker
11 | import com.pedro.common.VideoCodec
12 | import com.pedro.library.base.Camera1Base
13 | import com.pedro.library.view.OpenGlView
14 | import com.pedro.rtspserver.server.RtspServer
15 | import com.pedro.rtspserver.util.RtspServerStreamClient
16 | import java.nio.ByteBuffer
17 |
18 | /**
19 | * Created by pedro on 13/02/19.
20 | */
21 | class RtspServerCamera1: Camera1Base {
22 |
23 | private val rtspServer: RtspServer
24 |
25 | constructor(surfaceView: SurfaceView, connectChecker: ConnectChecker, port: Int): super(surfaceView) {
26 | rtspServer = RtspServer(connectChecker, port)
27 | }
28 |
29 | constructor(textureView: TextureView, connectCheckerRtsp: ConnectChecker, port: Int): super(textureView) {
30 | rtspServer = RtspServer(connectCheckerRtsp, port)
31 | }
32 |
33 | @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
34 | constructor(openGlView: OpenGlView, connectChecker: ConnectChecker, port: Int): super(openGlView) {
35 | rtspServer = RtspServer(connectChecker, port)
36 | }
37 |
38 | @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
39 | constructor(context: Context, connectChecker: ConnectChecker, port: Int): super(context) {
40 | rtspServer = RtspServer(connectChecker, port)
41 | }
42 |
43 | fun startStream() {
44 | super.startStream("")
45 | rtspServer.startServer()
46 | }
47 |
48 | override fun prepareAudioRtp(isStereo: Boolean, sampleRate: Int) {
49 | rtspServer.setAudioInfo(sampleRate, isStereo)
50 | }
51 |
52 | override fun startStreamRtp(url: String) { //unused
53 | }
54 |
55 | override fun stopStreamRtp() {
56 | rtspServer.stopServer()
57 | }
58 |
59 | override fun getAacDataRtp(aacBuffer: ByteBuffer, info: MediaCodec.BufferInfo) {
60 | rtspServer.sendAudio(aacBuffer, info)
61 | }
62 |
63 | override fun onSpsPpsVpsRtp(sps: ByteBuffer, pps: ByteBuffer?, vps: ByteBuffer?) {
64 | val newSps = sps.duplicate()
65 | val newPps = pps?.duplicate()
66 | val newVps = vps?.duplicate()
67 | rtspServer.setVideoInfo(newSps, newPps, newVps)
68 | }
69 |
70 | override fun getH264DataRtp(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) {
71 | rtspServer.sendVideo(h264Buffer, info)
72 | }
73 |
74 | override fun getStreamClient(): RtspServerStreamClient = RtspServerStreamClient(rtspServer)
75 |
76 | override fun setVideoCodecImp(codec: VideoCodec) {
77 | rtspServer.setVideoCodec(codec)
78 | }
79 |
80 | override fun setAudioCodecImp(codec: AudioCodec) {
81 | rtspServer.setAudioCodec(codec);
82 | }
83 | }
--------------------------------------------------------------------------------
/rtspserver/src/main/java/com/pedro/rtspserver/RtspServerCamera2.kt:
--------------------------------------------------------------------------------
1 | package com.pedro.rtspserver
2 |
3 | import android.content.Context
4 | import android.media.MediaCodec
5 | import android.os.Build
6 | import androidx.annotation.RequiresApi
7 | import com.pedro.common.AudioCodec
8 | import com.pedro.common.ConnectChecker
9 | import com.pedro.common.VideoCodec
10 | import com.pedro.library.base.Camera2Base
11 | import com.pedro.library.view.OpenGlView
12 | import com.pedro.rtspserver.server.RtspServer
13 | import com.pedro.rtspserver.util.RtspServerStreamClient
14 | import java.nio.ByteBuffer
15 |
16 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
17 | class RtspServerCamera2: Camera2Base {
18 |
19 | private val rtspServer: RtspServer
20 |
21 | constructor(openGlView: OpenGlView, connectChecker: ConnectChecker, port: Int): super(openGlView) {
22 | rtspServer = RtspServer(connectChecker, port)
23 | }
24 |
25 | constructor(context: Context, useOpengl: Boolean, connectCheckerRtsp: ConnectChecker, port: Int): super(context, useOpengl) {
26 | rtspServer = RtspServer(connectCheckerRtsp, port)
27 | }
28 |
29 | fun startStream() {
30 | super.startStream("")
31 | rtspServer.startServer()
32 | }
33 |
34 | override fun prepareAudioRtp(isStereo: Boolean, sampleRate: Int) {
35 | rtspServer.setAudioInfo(sampleRate, isStereo)
36 | }
37 |
38 | override fun startStreamRtp(url: String) { //unused
39 | }
40 |
41 | override fun stopStreamRtp() {
42 | rtspServer.stopServer()
43 | }
44 |
45 | override fun getAacDataRtp(aacBuffer: ByteBuffer, info: MediaCodec.BufferInfo) {
46 | rtspServer.sendAudio(aacBuffer, info)
47 | }
48 |
49 | override fun onSpsPpsVpsRtp(sps: ByteBuffer, pps: ByteBuffer?, vps: ByteBuffer?) {
50 | val newSps = sps.duplicate()
51 | val newPps = pps?.duplicate()
52 | val newVps = vps?.duplicate()
53 | rtspServer.setVideoInfo(newSps, newPps, newVps)
54 | }
55 |
56 | override fun getH264DataRtp(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) {
57 | rtspServer.sendVideo(h264Buffer, info)
58 | }
59 |
60 | override fun getStreamClient(): RtspServerStreamClient = RtspServerStreamClient(rtspServer)
61 |
62 | override fun setVideoCodecImp(codec: VideoCodec) {
63 | rtspServer.setVideoCodec(codec)
64 | }
65 |
66 | override fun setAudioCodecImp(codec: AudioCodec) {
67 | rtspServer.setAudioCodec(codec);
68 | }
69 | }
--------------------------------------------------------------------------------
/rtspserver/src/main/java/com/pedro/rtspserver/RtspServerDisplay.kt:
--------------------------------------------------------------------------------
1 | package com.pedro.rtspserver
2 |
3 | import android.content.Context
4 | import android.media.MediaCodec
5 | import android.os.Build
6 | import androidx.annotation.RequiresApi
7 | import com.pedro.common.AudioCodec
8 | import com.pedro.common.ConnectChecker
9 | import com.pedro.common.VideoCodec
10 | import com.pedro.library.base.DisplayBase
11 | import com.pedro.rtspserver.server.RtspServer
12 | import com.pedro.rtspserver.util.RtspServerStreamClient
13 | import java.nio.ByteBuffer
14 |
15 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
16 | class RtspServerDisplay(
17 | context: Context, useOpengl: Boolean,
18 | connectChecker: ConnectChecker, port: Int
19 | ): DisplayBase(context, useOpengl) {
20 |
21 | private val rtspServer: RtspServer = RtspServer(connectChecker, port)
22 |
23 | fun startStream() {
24 | super.startStream("")
25 | rtspServer.startServer()
26 | }
27 |
28 | override fun prepareAudioRtp(isStereo: Boolean, sampleRate: Int) {
29 | rtspServer.setAudioInfo(sampleRate, isStereo)
30 | }
31 |
32 | override fun startStreamRtp(url: String) { //unused
33 | }
34 |
35 | override fun stopStreamRtp() {
36 | rtspServer.stopServer()
37 | }
38 |
39 | override fun getAacDataRtp(aacBuffer: ByteBuffer, info: MediaCodec.BufferInfo) {
40 | rtspServer.sendAudio(aacBuffer, info)
41 | }
42 |
43 | override fun onSpsPpsVpsRtp(sps: ByteBuffer, pps: ByteBuffer?, vps: ByteBuffer?) {
44 | val newSps = sps.duplicate()
45 | val newPps = pps?.duplicate()
46 | val newVps = vps?.duplicate()
47 | rtspServer.setVideoInfo(newSps, newPps, newVps)
48 | }
49 |
50 | override fun getH264DataRtp(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) {
51 | rtspServer.sendVideo(h264Buffer, info)
52 | }
53 |
54 | override fun getStreamClient(): RtspServerStreamClient = RtspServerStreamClient(rtspServer)
55 |
56 | override fun setVideoCodecImp(codec: VideoCodec) {
57 | rtspServer.setVideoCodec(codec)
58 | }
59 |
60 | override fun setAudioCodecImp(codec: AudioCodec) {
61 | rtspServer.setAudioCodec(codec);
62 | }
63 | }
--------------------------------------------------------------------------------
/rtspserver/src/main/java/com/pedro/rtspserver/RtspServerFromFile.kt:
--------------------------------------------------------------------------------
1 | package com.pedro.rtspserver
2 |
3 | import android.content.Context
4 | import android.media.MediaCodec
5 | import android.os.Build
6 | import androidx.annotation.RequiresApi
7 | import com.pedro.common.AudioCodec
8 | import com.pedro.common.ConnectChecker
9 | import com.pedro.common.VideoCodec
10 | import com.pedro.encoder.input.decoder.AudioDecoderInterface
11 | import com.pedro.encoder.input.decoder.VideoDecoderInterface
12 | import com.pedro.library.base.FromFileBase
13 | import com.pedro.library.view.OpenGlView
14 | import com.pedro.rtspserver.server.RtspServer
15 | import com.pedro.rtspserver.util.RtspServerStreamClient
16 | import java.nio.ByteBuffer
17 |
18 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
19 | class RtspServerFromFile: FromFileBase {
20 |
21 | private val rtspServer: RtspServer
22 |
23 | constructor(openGlView: OpenGlView, connectCheckerRtsp: ConnectChecker, port: Int,
24 | videoDecoderInterface: VideoDecoderInterface,
25 | audioDecoderInterface: AudioDecoderInterface): super(openGlView, videoDecoderInterface,
26 | audioDecoderInterface) {
27 | rtspServer = RtspServer(connectCheckerRtsp, port)
28 | }
29 |
30 | constructor(context: Context, connectCheckerRtsp: ConnectChecker, port: Int,
31 | videoDecoderInterface: VideoDecoderInterface,
32 | audioDecoderInterface: AudioDecoderInterface): super(context, videoDecoderInterface,
33 | audioDecoderInterface) {
34 | rtspServer = RtspServer(connectCheckerRtsp, port)
35 | }
36 |
37 | fun startStream() {
38 | super.startStream("")
39 | rtspServer.startServer()
40 | }
41 |
42 | override fun prepareAudioRtp(isStereo: Boolean, sampleRate: Int) {
43 | rtspServer.setAudioInfo(sampleRate, isStereo)
44 | }
45 |
46 | override fun startStreamRtp(url: String) { //unused
47 | }
48 |
49 | override fun stopStreamRtp() {
50 | rtspServer.stopServer()
51 | }
52 |
53 | override fun getAacDataRtp(aacBuffer: ByteBuffer, info: MediaCodec.BufferInfo) {
54 | rtspServer.sendAudio(aacBuffer, info)
55 | }
56 |
57 | override fun onSpsPpsVpsRtp(sps: ByteBuffer, pps: ByteBuffer?, vps: ByteBuffer?) {
58 | val newSps = sps.duplicate()
59 | val newPps = pps?.duplicate()
60 | val newVps = vps?.duplicate()
61 | rtspServer.setVideoInfo(newSps, newPps, newVps)
62 | }
63 |
64 | override fun getH264DataRtp(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) {
65 | rtspServer.sendVideo(h264Buffer, info)
66 | }
67 |
68 | override fun getStreamClient(): RtspServerStreamClient = RtspServerStreamClient(rtspServer)
69 |
70 | override fun setVideoCodecImp(codec: VideoCodec) {
71 | rtspServer.setVideoCodec(codec)
72 | }
73 |
74 | override fun setAudioCodecImp(codec: AudioCodec) {
75 | rtspServer.setAudioCodec(codec);
76 | }
77 | }
--------------------------------------------------------------------------------
/rtspserver/src/main/java/com/pedro/rtspserver/RtspServerOnlyAudio.kt:
--------------------------------------------------------------------------------
1 | package com.pedro.rtspserver
2 |
3 | import android.media.MediaCodec
4 | import com.pedro.common.AudioCodec
5 | import com.pedro.common.ConnectChecker
6 | import com.pedro.library.base.OnlyAudioBase
7 | import com.pedro.rtspserver.server.RtspServer
8 | import com.pedro.rtspserver.util.RtspServerStreamClient
9 | import java.nio.ByteBuffer
10 |
11 | /**
12 | * Created by pedro on 17/04/21.
13 | */
14 | class RtspServerOnlyAudio(
15 | connectChecker: ConnectChecker, port: Int
16 | ): OnlyAudioBase() {
17 |
18 | private val rtspServer = RtspServer(connectChecker, port).apply {
19 | setOnlyAudio(true)
20 | }
21 |
22 | fun startStream() {
23 | super.startStream("")
24 | rtspServer.startServer()
25 | }
26 |
27 | override fun prepareAudioRtp(isStereo: Boolean, sampleRate: Int) {
28 | rtspServer.setAudioInfo(sampleRate, isStereo)
29 | }
30 |
31 | override fun startStreamRtp(url: String) { //unused
32 | }
33 |
34 | override fun stopStreamRtp() {
35 | rtspServer.stopServer()
36 | }
37 |
38 | override fun getAacDataRtp(aacBuffer: ByteBuffer, info: MediaCodec.BufferInfo) {
39 | rtspServer.sendAudio(aacBuffer, info)
40 | }
41 |
42 | override fun getStreamClient(): RtspServerStreamClient = RtspServerStreamClient(rtspServer)
43 |
44 | override fun setAudioCodecImp(codec: AudioCodec) {
45 | rtspServer.setAudioCodec(codec);
46 | }
47 | }
--------------------------------------------------------------------------------
/rtspserver/src/main/java/com/pedro/rtspserver/RtspServerStream.kt:
--------------------------------------------------------------------------------
1 | package com.pedro.rtspserver
2 |
3 | import android.content.Context
4 | import android.media.MediaCodec
5 | import android.os.Build
6 | import androidx.annotation.RequiresApi
7 | import com.pedro.common.AudioCodec
8 | import com.pedro.common.ConnectChecker
9 | import com.pedro.common.VideoCodec
10 | import com.pedro.library.base.StreamBase
11 | import com.pedro.library.util.sources.audio.AudioSource
12 | import com.pedro.library.util.sources.audio.MicrophoneSource
13 | import com.pedro.library.util.sources.video.Camera2Source
14 | import com.pedro.library.util.sources.video.VideoSource
15 | import com.pedro.rtspserver.server.RtspServer
16 | import com.pedro.rtspserver.util.RtspServerStreamClient
17 | import java.nio.ByteBuffer
18 |
19 | /**
20 | * Created by pedro on 13/02/19.
21 | */
22 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
23 | class RtspServerStream(
24 | context: Context, port: Int, connectChecker: ConnectChecker,
25 | videoSource: VideoSource, audioSource: AudioSource,
26 | ): StreamBase(context, videoSource, audioSource) {
27 |
28 | private val rtspServer = RtspServer(connectChecker, port)
29 |
30 | constructor(context: Context, port: Int, connectChecker: ConnectChecker):
31 | this(context, port, connectChecker, Camera2Source(context), MicrophoneSource())
32 |
33 | fun startStream() {
34 | super.startStream("")
35 | rtspServer.startServer()
36 | }
37 |
38 | override fun audioInfo(sampleRate: Int, isStereo: Boolean) {
39 | rtspServer.setAudioInfo(sampleRate, isStereo)
40 | }
41 |
42 | override fun rtpStartStream(endPoint: String) { //unused
43 | }
44 |
45 | override fun rtpStopStream() {
46 | rtspServer.stopServer()
47 | }
48 |
49 | override fun getAacDataRtp(aacBuffer: ByteBuffer, info: MediaCodec.BufferInfo) {
50 | rtspServer.sendAudio(aacBuffer, info)
51 | }
52 |
53 | override fun onSpsPpsVpsRtp(sps: ByteBuffer, pps: ByteBuffer?, vps: ByteBuffer?) {
54 | val newSps = sps.duplicate()
55 | val newPps = pps?.duplicate()
56 | val newVps = vps?.duplicate()
57 | rtspServer.setVideoInfo(newSps, newPps, newVps)
58 | }
59 |
60 | override fun getH264DataRtp(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) {
61 | rtspServer.sendVideo(h264Buffer, info)
62 | }
63 |
64 | override fun getStreamClient(): RtspServerStreamClient = RtspServerStreamClient(rtspServer)
65 |
66 | override fun setVideoCodecImp(codec: VideoCodec) {
67 | rtspServer.setVideoCodec(codec)
68 | }
69 |
70 | override fun setAudioCodecImp(codec: AudioCodec) {
71 | rtspServer.setAudioCodec(codec);
72 | }
73 | }
--------------------------------------------------------------------------------
/rtspserver/src/main/java/com/pedro/rtspserver/server/ClientListener.kt:
--------------------------------------------------------------------------------
1 | package com.pedro.rtspserver.server
2 |
3 | /**
4 | * Created by pedro on 20/12/23.
5 | */
6 | interface ClientListener {
7 |
8 | fun onClientConnected(client: ServerClient)
9 |
10 | fun onClientDisconnected(client: ServerClient)
11 | }
--------------------------------------------------------------------------------
/rtspserver/src/main/java/com/pedro/rtspserver/server/IpType.kt:
--------------------------------------------------------------------------------
1 | package com.pedro.rtspserver.server
2 |
3 | /**
4 | * Created by pedro on 23/1/24.
5 | */
6 | enum class IpType {
7 | IPv4, IPv6, All
8 | }
--------------------------------------------------------------------------------
/rtspserver/src/main/java/com/pedro/rtspserver/server/RtspServer.kt:
--------------------------------------------------------------------------------
1 | package com.pedro.rtspserver.server
2 |
3 | import android.media.MediaCodec
4 | import android.util.Log
5 | import com.pedro.common.AudioCodec
6 | import com.pedro.common.ConnectChecker
7 | import com.pedro.common.VideoCodec
8 | import com.pedro.common.onMainThreadHandler
9 | import com.pedro.rtsp.utils.RtpConstants
10 | import java.io.*
11 | import java.lang.RuntimeException
12 | import java.net.*
13 | import java.nio.ByteBuffer
14 | import java.util.concurrent.Semaphore
15 | import java.util.concurrent.TimeUnit
16 |
17 | /**
18 | *
19 | * Created by pedro on 13/02/19.
20 | *
21 | *
22 | * TODO Use different session per client.
23 | */
24 |
25 | class RtspServer(
26 | private val connectChecker: ConnectChecker,
27 | val port: Int
28 | ): ServerListener {
29 |
30 | private val TAG = "RtspServer"
31 | private var server: ServerSocket? = null
32 | val serverIp: String get() = getIPAddress()
33 | private val clients = mutableListOf()
34 | private var thread: Thread? = null
35 | private var running = false
36 | private var isEnableLogs = true
37 | private val semaphore = Semaphore(0)
38 | private val serverCommandManager = ServerCommandManager()
39 | private var clientListener: ClientListener? = null
40 | private var ipType = IpType.All
41 |
42 | val droppedAudioFrames: Long
43 | get() = synchronized(clients) {
44 | var items = 0L
45 | clients.forEach { items += it.droppedAudioFrames }
46 | return items
47 | }
48 |
49 | val droppedVideoFrames: Long
50 | get() = synchronized(clients) {
51 | var items = 0L
52 | clients.forEach { items += it.droppedVideoFrames }
53 | return items
54 | }
55 |
56 | val cacheSize: Int
57 | get() = synchronized(clients) {
58 | var items = 0
59 | clients.forEach { items += it.cacheSize }
60 | return items / getNumClients()
61 | }
62 | val sentAudioFrames: Long
63 | get() = synchronized(clients) {
64 | var items = 0L
65 | clients.forEach { items += it.sentAudioFrames }
66 | return items
67 | }
68 | val sentVideoFrames: Long
69 | get() = synchronized(clients) {
70 | var items = 0L
71 | clients.forEach { items += it.sentVideoFrames }
72 | return items
73 | }
74 |
75 | fun setClientListener(clientListener: ClientListener?) {
76 | this.clientListener = clientListener
77 | }
78 |
79 | fun setAuth(user: String?, password: String?) {
80 | serverCommandManager.setAuth(user, password)
81 | }
82 |
83 | fun startServer() {
84 | stopServer()
85 | thread = Thread {
86 | try {
87 | if (!serverCommandManager.videoDisabled) {
88 | if (!serverCommandManager.videoInfoReady()) {
89 | semaphore.drainPermits()
90 | Log.i(TAG, "waiting for video info")
91 | semaphore.tryAcquire(5000, TimeUnit.MILLISECONDS)
92 | }
93 | if (!serverCommandManager.videoInfoReady()) {
94 | onMainThreadHandler {
95 | connectChecker.onConnectionFailed("video info is null")
96 | }
97 | return@Thread
98 | }
99 | }
100 | server = ServerSocket(port)
101 | } catch (e: IOException) {
102 | onMainThreadHandler {
103 | connectChecker.onConnectionFailed("Server creation failed")
104 | }
105 | Log.e(TAG, "Error", e)
106 | return@Thread
107 | }
108 | Log.i(TAG, "Server started $serverIp:$port")
109 | while (!Thread.interrupted()) {
110 | try {
111 | val clientSocket = server?.accept() ?: continue
112 | val clientAddress = clientSocket.inetAddress.hostAddress
113 | if (clientAddress == null) {
114 | Log.e(TAG, "Unknown client ip, closing clientSocket...")
115 | if (!clientSocket.isClosed) clientSocket.close()
116 | continue
117 | }
118 | val client = ServerClient(clientSocket, serverIp, port, connectChecker, clientAddress,
119 | serverCommandManager, this)
120 | client.setLogs(isEnableLogs)
121 | client.start()
122 | synchronized(clients) {
123 | clients.add(client)
124 | }
125 | onMainThreadHandler {
126 | clientListener?.onClientConnected(client)
127 | }
128 | } catch (e: SocketException) {
129 | // server.close called
130 | break
131 | } catch (e: IOException) {
132 | Log.e(TAG, "Error", e)
133 | continue
134 | }
135 | }
136 | Log.i(TAG, "Server finished")
137 | }
138 | running = true
139 | thread?.start()
140 | }
141 |
142 | fun getNumClients(): Int = clients.size
143 |
144 | fun stopServer() {
145 | synchronized(clients) {
146 | clients.forEach { it.stopClient() }
147 | clients.clear()
148 | }
149 | if (server?.isClosed == false) server?.close()
150 | thread?.interrupt()
151 | try {
152 | thread?.join(100)
153 | } catch (e: InterruptedException) {
154 | thread?.interrupt()
155 | }
156 | semaphore.release()
157 | running = false
158 | thread = null
159 | }
160 |
161 | fun isRunning(): Boolean = running
162 |
163 | fun setOnlyAudio(onlyAudio: Boolean) {
164 | if (onlyAudio) {
165 | RtpConstants.trackAudio = 0
166 | RtpConstants.trackVideo = 1
167 | } else {
168 | RtpConstants.trackVideo = 0
169 | RtpConstants.trackAudio = 1
170 | }
171 | serverCommandManager.audioDisabled = false
172 | serverCommandManager.videoDisabled = onlyAudio
173 | }
174 |
175 | fun setOnlyVideo(onlyVideo: Boolean) {
176 | RtpConstants.trackVideo = 0
177 | RtpConstants.trackAudio = 1
178 | serverCommandManager.videoDisabled = false
179 | serverCommandManager.audioDisabled = onlyVideo
180 | }
181 |
182 | fun setLogs(enable: Boolean) {
183 | isEnableLogs = enable;
184 | synchronized(clients) {
185 | clients.forEach { it.setLogs(enable) }
186 | }
187 | }
188 |
189 | fun sendVideo(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) {
190 | synchronized(clients) {
191 | clients.forEach {
192 | if (it.isAlive && it.canSend && !serverCommandManager.videoDisabled) {
193 | it.sendVideoFrame(h264Buffer.duplicate(), info)
194 | }
195 | }
196 | }
197 | }
198 |
199 | fun sendAudio(aacBuffer: ByteBuffer, info: MediaCodec.BufferInfo) {
200 | synchronized(clients) {
201 | clients.forEach {
202 | if (it.isAlive && it.canSend && !serverCommandManager.audioDisabled) {
203 | it.sendAudioFrame(aacBuffer.duplicate(), info)
204 | }
205 | }
206 | }
207 | }
208 |
209 | fun setVideoInfo(sps: ByteBuffer, pps: ByteBuffer?, vps: ByteBuffer?) {
210 | serverCommandManager.setVideoInfo(sps, pps, vps)
211 | semaphore.release()
212 | }
213 |
214 | fun setAudioInfo(sampleRate: Int, isStereo: Boolean) {
215 | serverCommandManager.setAudioInfo(sampleRate, isStereo)
216 | }
217 |
218 | fun setVideoCodec(videoCodec: VideoCodec) {
219 | if (!isRunning()) {
220 | serverCommandManager.videoCodec = videoCodec
221 | } else {
222 | throw RuntimeException("Please set VideoCodec before startServer.")
223 | }
224 | }
225 |
226 | fun setAudioCodec(audioCodec: AudioCodec) {
227 | if (!isRunning()) {
228 | serverCommandManager.audioCodec = audioCodec
229 | } else {
230 | throw RuntimeException("Please set AudioCodec before startServer.")
231 | }
232 | }
233 |
234 | fun hasCongestion(percentUsed: Float): Boolean {
235 | synchronized(clients) {
236 | var congestion = false
237 | clients.forEach { if (it.hasCongestion(percentUsed)) congestion = true }
238 | return congestion
239 | }
240 | }
241 |
242 | fun resetSentAudioFrames() {
243 | synchronized(clients) {
244 | clients.forEach { it.resetSentAudioFrames() }
245 | }
246 | }
247 |
248 | fun resetSentVideoFrames() {
249 | synchronized(clients) {
250 | clients.forEach { it.resetSentVideoFrames() }
251 | }
252 | }
253 |
254 | fun resetDroppedAudioFrames() {
255 | synchronized(clients) {
256 | clients.forEach { it.resetDroppedAudioFrames() }
257 | }
258 | }
259 |
260 | fun resetDroppedVideoFrames() {
261 | synchronized(clients) {
262 | clients.forEach { it.resetDroppedVideoFrames() }
263 | }
264 | }
265 |
266 | @Throws(RuntimeException::class)
267 | fun resizeCache(newSize: Int) {
268 | synchronized(clients) {
269 | clients.forEach { it.resizeCache(newSize) }
270 | }
271 | }
272 |
273 | fun clearCache() {
274 | synchronized(clients) {
275 | clients.forEach { it.clearCache() }
276 | }
277 | }
278 |
279 | fun getItemsInCache(): Int {
280 | synchronized(clients) {
281 | var items = 0
282 | clients.forEach { items += it.getItemsInCache() }
283 | return items
284 | }
285 | }
286 |
287 | fun forceIpType(ipType: IpType) {
288 | if (!isRunning()) {
289 | this.ipType = ipType
290 | } else {
291 | throw RuntimeException("Please set IpType before startServer.")
292 | }
293 | }
294 |
295 | override fun onClientDisconnected(client: ServerClient) {
296 | synchronized(clients) {
297 | client.stopClient()
298 | clients.remove(client)
299 | onMainThreadHandler {
300 | clientListener?.onClientDisconnected(client)
301 | }
302 | }
303 | }
304 |
305 | private fun getIPAddress(): String {
306 | val interfaces: List = NetworkInterface.getNetworkInterfaces().toList()
307 | val vpnInterfaces = interfaces.filter { it.displayName.contains(VPN_INTERFACE) }
308 | val address: String by lazy { interfaces.findAddress().firstOrNull() ?: DEFAULT_IP }
309 | return if (vpnInterfaces.isNotEmpty()) {
310 | val vpnAddresses = vpnInterfaces.findAddress()
311 | vpnAddresses.firstOrNull() ?: address
312 | } else {
313 | address
314 | }
315 | }
316 |
317 | private fun List.findAddress(): List = this.asSequence()
318 | .map { addresses -> addresses.inetAddresses.asSequence() }
319 | .flatten()
320 | .filter { address -> !address.isLoopbackAddress }
321 | .map { it.hostAddress }
322 | .filter { address ->
323 | //exclude invalid IPv6 addresses
324 | address?.startsWith("fe80") != true && // Exclude link-local addresses
325 | address?.startsWith("fc00") != true && // Exclude unique local addresses
326 | address?.startsWith("fd00") != true // Exclude unique local addresses
327 | }
328 | .filter { address ->
329 | when (ipType) {
330 | IpType.IPv4 -> address?.contains(":") == false
331 | IpType.IPv6 -> address?.contains(":") == true
332 | IpType.All -> true
333 | }
334 | }
335 | .toList()
336 |
337 | companion object {
338 | private const val VPN_INTERFACE = "tun"
339 | private const val DEFAULT_IP = "0.0.0.0"
340 | }
341 | }
--------------------------------------------------------------------------------
/rtspserver/src/main/java/com/pedro/rtspserver/server/ServerClient.kt:
--------------------------------------------------------------------------------
1 | package com.pedro.rtspserver.server
2 |
3 | import android.media.MediaCodec
4 | import android.util.Log
5 | import com.pedro.common.ConnectChecker
6 | import com.pedro.common.onMainThreadHandler
7 | import com.pedro.rtsp.rtsp.Protocol
8 | import com.pedro.rtsp.rtsp.RtspSender
9 | import com.pedro.rtsp.rtsp.commands.Method
10 | import kotlinx.coroutines.CoroutineScope
11 | import kotlinx.coroutines.Dispatchers
12 | import kotlinx.coroutines.launch
13 | import kotlinx.coroutines.withContext
14 | import java.io.BufferedReader
15 | import java.io.BufferedWriter
16 | import java.io.InputStreamReader
17 | import java.io.OutputStreamWriter
18 | import java.net.Socket
19 | import java.net.SocketException
20 | import java.nio.ByteBuffer
21 |
22 | class ServerClient(
23 | private val socket: Socket, serverIp: String, serverPort: Int,
24 | private val connectChecker: ConnectChecker,
25 | val clientAddress: String,
26 | private val serverCommandManager: ServerCommandManager,
27 | private val listener: ServerListener
28 | ): Thread() {
29 |
30 | private val TAG = "Client"
31 | private val output = BufferedWriter(OutputStreamWriter(socket.getOutputStream()))
32 | private val input = BufferedReader(InputStreamReader(socket.getInputStream()))
33 | private val rtspSender = RtspSender(connectChecker, serverCommandManager)
34 | var canSend = false
35 | private set
36 |
37 | val droppedAudioFrames: Long
38 | get() = rtspSender.droppedAudioFrames
39 | val droppedVideoFrames: Long
40 | get() = rtspSender.droppedVideoFrames
41 |
42 | val cacheSize: Int
43 | get() = rtspSender.getCacheSize()
44 | val sentAudioFrames: Long
45 | get() = rtspSender.getSentAudioFrames()
46 | val sentVideoFrames: Long
47 | get() = rtspSender.getSentVideoFrames()
48 |
49 | init {
50 | serverCommandManager.setServerInfo(serverIp, serverPort)
51 | }
52 |
53 | override fun run() {
54 | super.run()
55 | Log.i(TAG, "New client $clientAddress")
56 | while (!interrupted()) {
57 | try {
58 | val request = serverCommandManager.getRequest(input)
59 | val cSeq = request.cSeq //update cSeq
60 | if (cSeq == -1) { //If cSeq parsed fail send error to client
61 | output.write(serverCommandManager.createError(500, cSeq))
62 | output.flush()
63 | continue
64 | }
65 | val response = serverCommandManager.createResponse(request.method, request.text, cSeq, clientAddress)
66 | Log.i(TAG, response)
67 | output.write(response)
68 | output.flush()
69 |
70 | if (request.method == Method.PLAY) {
71 | Log.i(TAG, "Protocol ${serverCommandManager.protocol}")
72 | rtspSender.setSocketsInfo(serverCommandManager.protocol, serverCommandManager.videoServerPorts,
73 | serverCommandManager.audioServerPorts)
74 | if (!serverCommandManager.videoDisabled) {
75 | rtspSender.setVideoInfo(serverCommandManager.sps!!, serverCommandManager.pps, serverCommandManager.vps)
76 | }
77 | if (!serverCommandManager.audioDisabled) {
78 | rtspSender.setAudioInfo(serverCommandManager.sampleRate)
79 | }
80 | rtspSender.setDataStream(socket.getOutputStream(), clientAddress)
81 | if (serverCommandManager.protocol == Protocol.UDP) {
82 | if (!serverCommandManager.videoDisabled) {
83 | rtspSender.setVideoPorts(serverCommandManager.videoPorts[0], serverCommandManager.videoPorts[1])
84 | }
85 | if (!serverCommandManager.audioDisabled) {
86 | rtspSender.setAudioPorts(serverCommandManager.audioPorts[0], serverCommandManager.audioPorts[1])
87 | }
88 | }
89 | rtspSender.start()
90 | onMainThreadHandler {
91 | connectChecker.onConnectionSuccess()
92 | }
93 | canSend = true
94 | } else if (request.method == Method.TEARDOWN) {
95 | Log.i(TAG, "Client disconnected")
96 | listener.onClientDisconnected(this)
97 | onMainThreadHandler {
98 | connectChecker.onDisconnect()
99 | }
100 | }
101 | } catch (e: SocketException) { // Client has left
102 | Log.e(TAG, "Client disconnected", e)
103 | listener.onClientDisconnected(this)
104 | break
105 | } catch (e: Exception) {
106 | Log.e(TAG, "Unexpected error", e)
107 | }
108 | }
109 | }
110 |
111 | fun stopClient() {
112 | CoroutineScope(Dispatchers.IO).launch {
113 | canSend = false
114 | rtspSender.stop()
115 | interrupt()
116 | withContext(Dispatchers.IO) {
117 | try {
118 | join(100)
119 | } catch (e: InterruptedException) {
120 | interrupt()
121 | } finally {
122 | socket.close()
123 | }
124 | }
125 | }
126 | }
127 |
128 | fun hasCongestion(percentUsed: Float): Boolean = rtspSender.hasCongestion(percentUsed)
129 |
130 | fun resetSentAudioFrames() {
131 | rtspSender.resetSentAudioFrames()
132 | }
133 |
134 | fun resetSentVideoFrames() {
135 | rtspSender.resetSentVideoFrames()
136 | }
137 |
138 | fun resetDroppedAudioFrames() {
139 | rtspSender.resetDroppedAudioFrames()
140 | }
141 |
142 | fun resetDroppedVideoFrames() {
143 | rtspSender.resetDroppedVideoFrames()
144 | }
145 |
146 | @Throws(RuntimeException::class)
147 | fun resizeCache(newSize: Int) {
148 | rtspSender.resizeCache(newSize)
149 | }
150 |
151 | fun setLogs(enable: Boolean) {
152 | rtspSender.setLogs(enable)
153 | }
154 |
155 | fun clearCache() {
156 | rtspSender.clearCache()
157 | }
158 |
159 | fun getItemsInCache(): Int = rtspSender.getItemsInCache()
160 |
161 | fun sendVideoFrame(h264Buffer: ByteBuffer, info: MediaCodec.BufferInfo) {
162 | rtspSender.sendVideoFrame(h264Buffer, info)
163 | }
164 |
165 | fun sendAudioFrame(aacBuffer: ByteBuffer, info: MediaCodec.BufferInfo) {
166 | rtspSender.sendAudioFrame(aacBuffer, info)
167 | }
168 | }
--------------------------------------------------------------------------------
/rtspserver/src/main/java/com/pedro/rtspserver/server/ServerCommandManager.kt:
--------------------------------------------------------------------------------
1 | package com.pedro.rtspserver.server
2 |
3 | import android.util.Log
4 | import com.pedro.common.AudioCodec
5 | import com.pedro.common.VideoCodec
6 | import com.pedro.rtsp.rtsp.Protocol
7 | import com.pedro.rtsp.rtsp.commands.Command
8 | import com.pedro.rtsp.rtsp.commands.CommandsManager
9 | import com.pedro.rtsp.rtsp.commands.Method
10 | import com.pedro.rtsp.rtsp.commands.SdpBody
11 | import com.pedro.rtsp.utils.RtpConstants
12 | import java.io.BufferedReader
13 | import java.io.IOException
14 | import java.net.SocketException
15 | import java.util.regex.Pattern
16 | import kotlin.io.encoding.Base64
17 | import kotlin.io.encoding.ExperimentalEncodingApi
18 |
19 | /**
20 | *
21 | * Created by pedro on 23/10/19.
22 | *
23 | */
24 | @OptIn(ExperimentalEncodingApi::class)
25 | class ServerCommandManager: CommandsManager() {
26 |
27 | private var serverIp: String = ""
28 | private var serverPort: Int = 0
29 |
30 | private val TAG = "ServerCommandManager"
31 | var audioPorts = ArrayList()
32 | var videoPorts = ArrayList()
33 |
34 | fun setServerInfo(serverIp: String, serverPort: Int) {
35 | this.serverIp = serverIp
36 | this.serverPort = serverPort
37 | }
38 |
39 | fun createResponse(method: Method, request: String, cSeq: Int, clientIp: String): String {
40 | return when (method){
41 | Method.OPTIONS -> createOptions(cSeq)
42 | Method.DESCRIBE -> {
43 | if (needAuth()) {
44 | val auth = getAuth(request)
45 | val data = "$user:$password"
46 | val base64Data = Base64.encode(data.toByteArray())
47 | if (base64Data.trim() == auth.trim()) {
48 | Log.i(TAG, "basic auth success")
49 | createDescribe(cSeq, clientIp) // auth accepted
50 | } else {
51 | Log.e(TAG, "basic auth error")
52 | createError(401, cSeq)
53 | }
54 | } else {
55 | createDescribe(cSeq, clientIp)
56 | }
57 | }
58 | Method.SETUP -> {
59 | val track = getTrack(request)
60 | if (track != null) {
61 | protocol = getProtocol(request, track)
62 | return when (protocol) {
63 | Protocol.TCP -> {
64 | createSetup(cSeq, track, clientIp)
65 | }
66 | Protocol.UDP -> {
67 | if (loadPorts(request, track)) createSetup(cSeq, track, clientIp) else createError(500, cSeq)
68 | }
69 | else -> {
70 | createError(500, cSeq)
71 | }
72 | }
73 | } else {
74 | createError(500, cSeq)
75 | }
76 | }
77 | Method.PLAY -> createPlay(cSeq)
78 | Method.PAUSE -> createPause(cSeq)
79 | Method.TEARDOWN -> createTeardown(cSeq)
80 | else -> createError(400, cSeq)
81 | }
82 | }
83 |
84 | private fun needAuth(): Boolean {
85 | return !user.isNullOrEmpty() && !password.isNullOrEmpty()
86 | }
87 |
88 | private fun getAuth(request: String): String {
89 | val rtspPattern = Pattern.compile("Authorization: Basic ([\\w+/=]+)")
90 | val matcher = rtspPattern.matcher(request)
91 | return if (matcher.find()) {
92 | matcher.group(1) ?: ""
93 | } else {
94 | ""
95 | }
96 | }
97 |
98 | private fun getProtocol(request: String, track: Int): Protocol {
99 | return if (request.contains("UDP", true) || loadPorts(request, track)) {
100 | Protocol.UDP
101 | } else {
102 | Protocol.TCP
103 | }
104 | }
105 |
106 | private fun loadPorts(request: String, track: Int): Boolean {
107 | val ports = ArrayList()
108 | val portsMatcher =
109 | Pattern.compile("client_port=(\\d+)(?:-(\\d+))?", Pattern.CASE_INSENSITIVE).matcher(request)
110 | if (portsMatcher.find()) {
111 | portsMatcher.group(1)?.toInt()?.let { ports.add(it) }
112 | portsMatcher.group(2)?.toInt()?.let { ports.add(it) }
113 | } else {
114 | Log.e(TAG, "UDP ports not found")
115 | return false
116 | }
117 | if (track == RtpConstants.trackAudio) { //audio ports
118 | audioPorts.clear()
119 | audioPorts.add(ports[0])
120 | audioPorts.add(ports[1])
121 | Log.i(TAG, "Audio ports: $audioPorts")
122 | } else { //video ports
123 | videoPorts.clear()
124 | videoPorts.add(ports[0])
125 | videoPorts.add(ports[1])
126 | Log.i(TAG, "Video ports: $videoPorts")
127 | }
128 | return true
129 | }
130 |
131 | private fun getTrack(request: String): Int? {
132 | val trackMatcher = Pattern.compile("streamid=(\\w+)", Pattern.CASE_INSENSITIVE).matcher(request)
133 | return if (trackMatcher.find()) {
134 | trackMatcher.group(1)?.toInt()
135 | } else {
136 | null
137 | }
138 | }
139 |
140 | @Throws(IOException::class, IllegalStateException::class, SocketException::class)
141 | fun getRequest(input: BufferedReader): Command {
142 | return super.getResponse(input, Method.UNKNOWN)
143 | }
144 |
145 | private fun createStatus(code: Int): String {
146 | return when (code) {
147 | 200 -> "200 OK"
148 | 400 -> "400 Bad Request"
149 | 401 -> "401 Unauthorized"
150 | 404 -> "404 Not Found"
151 | 500 -> "500 Internal Server Error"
152 | else -> "500 Internal Server Error"
153 | }
154 | }
155 |
156 | fun createError(code: Int, cSeq: Int): String {
157 | val auth = if (code == 401) {
158 | "WWW-Authenticate: Basic realm=\"pedroSG94\"\r\n"
159 | } else ""
160 | return "RTSP/1.0 ${createStatus(code)}\r\nServer: pedroSG94 Server\r\n${auth}Cseq: $cSeq\r\n\r\n"
161 | }
162 |
163 | private fun createHeader(cSeq: Int): String {
164 | return "RTSP/1.0 ${createStatus(200)}\r\nServer: pedroSG94 Server\r\nCseq: $cSeq\r\n"
165 | }
166 |
167 | private fun createOptions(cSeq: Int): String {
168 | return "${createHeader(cSeq)}Public: DESCRIBE,SETUP,TEARDOWN,PLAY,PAUSE\r\n\r\n"
169 | }
170 |
171 | private fun createDescribe(cSeq: Int, clientIp: String): String {
172 | val body = createBody(clientIp)
173 | return "${createHeader(cSeq)}Content-Length: ${body.length}\r\nContent-Base: rtsp://$serverIp:$serverPort/\r\nContent-Type: application/sdp\r\n\r\n$body"
174 | }
175 |
176 | private fun createBody(clientIp: String): String {
177 | var audioBody = ""
178 | if (!audioDisabled) {
179 | audioBody = when (audioCodec) {
180 | AudioCodec.AAC -> SdpBody.createAacBody(RtpConstants.trackAudio, sampleRate, isStereo)
181 | AudioCodec.G711 -> SdpBody.createG711Body(RtpConstants.trackAudio, sampleRate, isStereo)
182 | AudioCodec.OPUS -> SdpBody.createOpusBody(RtpConstants.trackAudio)
183 | }
184 | }
185 | var videoBody = ""
186 | if (!videoDisabled) {
187 | val sps = this.sps
188 | val pps = this.pps
189 | val vps = this.vps
190 | videoBody = when (videoCodec) {
191 | VideoCodec.H264 -> {
192 | if (sps == null || pps == null) throw IllegalArgumentException("sps or pps can't be null with h264")
193 | SdpBody.createH264Body(RtpConstants.trackVideo, encodeToString(sps), encodeToString(pps))
194 | }
195 | VideoCodec.H265 -> {
196 | if (sps == null || pps == null || vps == null) throw IllegalArgumentException("sps, pps or vps can't be null with h265")
197 | SdpBody.createH265Body(RtpConstants.trackVideo, encodeToString(sps), encodeToString(pps), encodeToString(vps))
198 | }
199 | VideoCodec.AV1 -> {
200 | SdpBody.createAV1Body(RtpConstants.trackVideo)
201 | }
202 | }
203 | }
204 | val ipVersion = if (serverIp.contains(":")) "IP6" else "IP4"
205 | return "v=0\r\no=- 0 0 IN $ipVersion $serverIp\r\ns=Unnamed\r\ni=N/A\r\nc=IN $ipVersion $clientIp\r\nt=0 0\r\na=recvonly\r\n$videoBody$audioBody\r\n"
206 | }
207 |
208 | private fun createSetup(cSeq: Int, track: Int, clientIp: String): String {
209 | val protocolSetup = if (protocol == Protocol.UDP) {
210 | val clientPorts = if (track == RtpConstants.trackAudio) audioPorts else videoPorts
211 | val serverPorts = if (track == RtpConstants.trackAudio) audioServerPorts else videoServerPorts
212 | "UDP;unicast;destination=$clientIp;client_port=${clientPorts[0]}-${clientPorts[1]};server_port=${serverPorts[0]}-${serverPorts[1]}"
213 | } else {
214 | "TCP;unicast;interleaved=" + (2 * track) + "-" + (2 * track + 1)
215 | }
216 | return "${createHeader(cSeq)}Content-Length: 0\r\nTransport: RTP/AVP/$protocolSetup;mode=play\r\nSession: 1185d20035702ca\r\nCache-Control: no-cache\r\n\r\n"
217 | }
218 |
219 | private fun createPlay(cSeq: Int): String {
220 | var info = ""
221 | if (!videoDisabled) {
222 | info += "url=rtsp://$serverIp:$serverPort/streamid=${RtpConstants.trackVideo};seq=1;rtptime=0"
223 | }
224 | if (!audioDisabled) {
225 | if (!videoDisabled) info += ","
226 | info += "url=rtsp://$serverIp:$serverPort/streamid=${RtpConstants.trackAudio};seq=1;rtptime=0"
227 | }
228 | return "${createHeader(cSeq)}Content-Length: 0\r\nRTP-Info: $info\r\nSession: 1185d20035702ca\r\n\r\n"
229 | }
230 |
231 | private fun createPause(cSeq: Int): String {
232 | return "${createHeader(cSeq)}Content-Length: 0\r\n\r\n"
233 | }
234 |
235 | private fun createTeardown(cSeq: Int): String {
236 | return "${createHeader(cSeq)}\r\n"
237 | }
238 |
239 | private fun encodeToString(bytes: ByteArray): String {
240 | return Base64.encode(bytes)
241 | }
242 | }
--------------------------------------------------------------------------------
/rtspserver/src/main/java/com/pedro/rtspserver/server/ServerListener.kt:
--------------------------------------------------------------------------------
1 | package com.pedro.rtspserver.server
2 |
3 | interface ServerListener {
4 | fun onClientDisconnected(client: ServerClient)
5 | }
--------------------------------------------------------------------------------
/rtspserver/src/main/java/com/pedro/rtspserver/util/RtspServerStreamClient.kt:
--------------------------------------------------------------------------------
1 | package com.pedro.rtspserver.util
2 |
3 | import com.pedro.library.util.streamclient.StreamBaseClient
4 | import com.pedro.rtspserver.server.ClientListener
5 | import com.pedro.rtspserver.server.IpType
6 | import com.pedro.rtspserver.server.RtspServer
7 |
8 | /**
9 | * Created by pedro on 20/12/23.
10 | */
11 | class RtspServerStreamClient(
12 | private val rtspServer: RtspServer,
13 | ): StreamBaseClient() {
14 |
15 | fun forceIpType(ipType: IpType) {
16 | rtspServer.forceIpType(ipType)
17 | }
18 |
19 | fun setClientListener(clientListener: ClientListener?) {
20 | rtspServer.setClientListener(clientListener)
21 | }
22 |
23 | fun getNumClients(): Int = rtspServer.getNumClients()
24 |
25 | fun getEndPointConnection(): String = "rtsp://${rtspServer.serverIp}:${rtspServer.port}/"
26 |
27 | override fun setAuthorization(user: String?, password: String?) {
28 | rtspServer.setAuth(user, password)
29 | }
30 |
31 | override fun setBitrateExponentialFactor(factor: Float) {
32 | }
33 |
34 | override fun setReTries(reTries: Int) {
35 | }
36 |
37 | override fun reTry(delay: Long, reason: String, backupUrl: String?): Boolean {
38 | return false
39 | }
40 |
41 | override fun hasCongestion(percentUsed: Float): Boolean = rtspServer.hasCongestion(percentUsed)
42 |
43 | override fun setLogs(enabled: Boolean) {
44 | rtspServer.setLogs(enabled)
45 | }
46 |
47 | override fun setCheckServerAlive(enabled: Boolean) {
48 | }
49 |
50 | override fun resizeCache(newSize: Int) {
51 | rtspServer.resizeCache(newSize)
52 | }
53 |
54 | override fun clearCache() {
55 | rtspServer.clearCache()
56 | }
57 |
58 | override fun getBitrateExponentialFactor(): Float = 1f
59 |
60 | override fun getCacheSize(): Int = rtspServer.cacheSize
61 |
62 | override fun getItemsInCache(): Int = rtspServer.getItemsInCache()
63 |
64 | override fun getSentAudioFrames(): Long = rtspServer.sentAudioFrames
65 |
66 | override fun getSentVideoFrames(): Long = rtspServer.sentVideoFrames
67 |
68 | override fun getDroppedAudioFrames(): Long = rtspServer.droppedAudioFrames
69 |
70 | override fun getDroppedVideoFrames(): Long = rtspServer.droppedVideoFrames
71 |
72 | override fun resetSentAudioFrames() {
73 | rtspServer.resetSentAudioFrames()
74 | }
75 |
76 | override fun resetSentVideoFrames() {
77 | rtspServer.resetSentVideoFrames()
78 | }
79 |
80 | override fun resetDroppedAudioFrames() {
81 | rtspServer.resetDroppedAudioFrames()
82 | }
83 |
84 | override fun resetDroppedVideoFrames() {
85 | rtspServer.resetDroppedVideoFrames()
86 | }
87 |
88 | override fun setOnlyAudio(onlyAudio: Boolean) {
89 | rtspServer.setOnlyAudio(onlyAudio)
90 | }
91 |
92 | override fun setOnlyVideo(onlyVideo: Boolean) {
93 | rtspServer.setOnlyVideo(onlyVideo)
94 | }
95 | }
--------------------------------------------------------------------------------
/rtspserver/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | rtspserver
3 |
4 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 |
15 | @Suppress("UnstableApiUsage")
16 | dependencyResolutionManagement {
17 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
18 | repositories {
19 | google()
20 | mavenCentral()
21 | maven { url = uri("https://jitpack.io") }
22 | }
23 | }
24 |
25 | rootProject.name = "FastRtspLive"
26 | include(":app")
27 | include(":rtspserver")
28 |
29 | //include(":camera_record")
30 | //project(":camera_record").projectDir = file("G:\\project\\Camreax-H264\\camera_record")
31 |
--------------------------------------------------------------------------------