├── .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 |