应用RTSP显示视频流

5 阅读4分钟

RTSP(Real Time Streaming Protocol)即实时流协议,是一种用于控制多媒体服务器向客户端传输实时数据的应用层协议。它本身并不传输数据,而是像一个 “指挥官”,负责建立、管理和控制媒体流的传输,常与 RTP(Real - time Transport Protocol)和 RTCP(Real - time Transport Control Protocol)协同工作。RTP 用于传输实际的媒体数据,RTCP 则用于监控传输质量和提供反馈信息。

1,在模块级的build.gradle.kts(:app)文件中:

plugins {
    ......
    kotlin("plugin.serialization") version "2.0.0"
}

android {
	......
    compileSdk = 35
	......
}

dependencies {
	implementation(libs.media3.ui)
  implementation(libs.media3.exoplayer.rtsp)
	implementation ("androidx.media3:media3-exoplayer:1.0.0")
}

在libs.versions.toml文件中

[versions]
media3Ui = "1.6.1"
media3ExoplayerRtsp = "1.6.1"

[libraries]
media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3Ui" }
media3-exoplayer-rtsp = { group = "androidx.media3", name = "media3-exoplayer-rtsp", version.ref = "media3ExoplayerRtsp" }

2,定义可序列号的数据类

// 导入了 kotlinx.serialization 库中的 Serializable 注解。
// kotlinx.serialization 是 Kotlin 官方的序列化库,借助该库,能把 Kotlin 对象序列化为多种格式(像 JSON、XML 等),也能把这些格式的数据反序列化为 Kotlin 对象。Serializable 注解的作用是标记某个类可被序列化。
import kotlinx.serialization.Serializable

// 注解用在类的声明之上,表明 EricRtspCameraConfig 类能够被序列化。
// 被该注解标记的类,其所有属性都会被序列化,除非这些属性被标记为 transient。
@Serializable
data class EricRtspCameraConfig(
    val id: String,
    val name: String,
    val rtspUrl: String,
    val username: String = "",
    val password: String = "",
    val useTcp: Boolean = true
)
// data class 是 Kotlin 里的一种特殊类,它会自动为类生成一些实用的方法,像 equals()、hashCode()、toString() 以及 copy() 等。通常,data class 用于存储数据。

3,定义视频显示类

import android.content.Context
import android.content.SharedPreferences
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.rtsp.RtspMediaSource
import androidx.media3.ui.PlayerView
import com.ark.smartpanel.R
import com.ark.smartpanel.camera.CameraViewerActivity
import kotlinx.serialization.json.Json
import java.util.UUID

class EricRtspStreamViewerActivity: AppCompatActivity() {

    private lateinit var sharedPreferences: SharedPreferences
    private lateinit var playerView: PlayerView
    private lateinit var loadingIndicator: ProgressBar
    private lateinit var errorText: TextView
    private var player: ExoPlayer? = null
    private var cameraConfig: EricRtspCameraConfig? = null
    private var cameraId: String = ""

    companion object {
        private const val TAG = "EricRtspStream"
        private const val PREFS_NAME = "eric_rtsp_camera"
        private const val KEY_CAMERAS = "eric_cameras"
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Log.v(TAG, "onCreate...")
        setContentView(R.layout.eric_rtsp_viewer)

        playerView = findViewById(R.id.playerView)
        loadingIndicator = findViewById(R.id.loading_indicator)
        errorText = findViewById(R.id.error_text)
        sharedPreferences = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)

        setCamera()

        val cameraId = cameraConfig?.id ?: cameraId
        Log.v(TAG, "onCreate...cameraId:$cameraId")
        if (cameraId != null) {
            cameraConfig = getCamera(cameraId)
            Log.v(TAG, "onCreate...cameraConfig:$cameraConfig")
        }
        initExoPlayer()
    }

    private fun initExoPlayer() {
        Log.v(TAG, "initExoPlayer...")
        // ExoPlayer.Builder 是用于构建 ExoPlayer 实例的构建器类
        // build() 方法调用后会根据构建器中的配置创建一个 ExoPlayer 实例
        // apply 是 Kotlin 的一个作用域函数,它会对调用它的对象(这里是 ExoPlayer 实例)执行一系列操作,并且返回该对象本身。
        player = ExoPlayer.Builder(this).build().apply {
            // playWhenReady = true 设置播放器在准备好后立即开始播放
            playWhenReady = true
            // 为播放器添加一个监听器 playerListener,当播放器的状态发生变化(如开始播放、暂停、结束等)时,监听器中的相应方法会被调用
            addListener(playerListener)
        }
        Log.v(TAG, "initExoPlayer...player:$player")

        // 设置 PlayerView
        // playerView 通常是一个 PlayerView 控件,它是 Android 中用于显示 ExoPlayer 播放内容的视图组件。
        // 将之前初始化好的 player 对象关联到 playerView 上,使得 playerView 能够显示 player 播放的媒体内容。
        playerView.player = player

        // 加载 RTSP 流
        loadRtspStream()
    }

    // 使用 Kotlin 的匿名对象语法创建一个实现 Player.Listener 接口的对象
    // Player.Listener 是 ExoPlayer 库中用于监听播放器状态变化和错误事件的接口
    private val playerListener = object : Player.Listener {
        // 当播放器的播放状态发生改变时,该方法会被调用
        override fun onPlaybackStateChanged(playbackState: Int) {
            when (playbackState) {
                Player.STATE_BUFFERING -> showLoadingView(true)
                Player.STATE_READY -> showLoadingView(false)
                Player.STATE_ENDED -> showErrorView("播放结束")
                Player.STATE_IDLE -> showLoadingView(false)
            }
        }

        // 当播放器发生错误时,该方法会被调用
        override fun onPlayerError(error: PlaybackException) {
            showErrorView("Play error: ${getErrorMessage(error)}")
            Log.v(CameraViewerActivity.TAG,"Play error: ${error.message}", error)
            if (!isNetworkConnected()) {
                showErrorView("network is not connected.")
                return
            }
            // 尝试重连,调用 prepare() 方法重新准备播放器,尝试重新连接并播放。
            player?.prepare()
        }
    }

    private fun isNetworkConnected(): Boolean {
        // getSystemService(Context.CONNECTIVITY_SERVICE):这是 Android 系统提供的一个方法,用于获取系统服务。
        // Context.CONNECTIVITY_SERVICE 是一个常量,代表连接服务。
        // 通过这个方法可以获取到与网络连接管理相关的服务对象。
        // as ConnectivityManager:使用类型转换操作符 as 将获取到的系统服务对象强制转换为 ConnectivityManager 类型。
        // ConnectivityManager 类是 Android 中用于管理网络连接的核心类,它提供了一系列方法来检查网络状态、获取网络信息等。
        val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        // 调用 ConnectivityManager 的 activeNetwork 属性,该属性返回当前设备所使用的活动网络对象。如果设备没有连接到任何网络,该属性将返回 null。
        val network = connectivityManager.activeNetwork
        // 调用 ConnectivityManager 的 getNetworkCapabilities 方法,传入当前活动网络对象 network,该方法会返回一个 NetworkCapabilities 对象,该对象包含了当前网络的各种能力信息,例如是否支持 Wi-Fi、蜂窝网络等。如果传入的 network 为 null,则该方法返回 null。
        val networkCapabilities = connectivityManager.getNetworkCapabilities(network)
        // 首先检查 networkCapabilities 是否为 null,如果为 null,说明当前没有有效的网络连接信息,直接返回 false。
        return networkCapabilities != null &&
                // 调用 NetworkCapabilities 对象的 hasTransport 方法,检查当前网络是否支持 Wi-Fi 传输。NetworkCapabilities.TRANSPORT_WIFI 是一个常量,表示 Wi-Fi 传输类型。
                (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
                        // 调用 hasTransport 方法,检查当前网络是否支持蜂窝网络传输。NetworkCapabilities.TRANSPORT_CELLULAR 是一个常量,表示蜂窝网络传输类型。
                        networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR))
    }

    private fun showLoadingView(show: Boolean) {
        loadingIndicator.visibility = if (show) View.VISIBLE else View.GONE
        if (show) {
            errorText.visibility = View.GONE
        }
    }

    private fun showErrorView(message: String) {
        errorText.text = message
        errorText.visibility = View.VISIBLE
        loadingIndicator.visibility = View.GONE
        Toast.makeText(this, message, Toast.LENGTH_LONG).show()
    }

    private fun getErrorMessage(error: PlaybackException): String {
        return when (error.errorCode) {
            PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED -> "网络连接失败"
            PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT -> "网络连接超时"
            PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED -> "不支持的流格式"
            PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> "HTTP状态错误"
            PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE -> "无效的内容类型"
            else -> "Error Code: ${error.errorCode}"
        }
    }

    // @OptIn(UnstableApi::class):这是一个注解,用于表明该函数使用了不稳定的 API。在 Kotlin 里,不稳定的 API 往往处于试验阶段,未来可能会有改动。
    @OptIn(UnstableApi::class)
    private fun loadRtspStream() {
        Log.v(TAG, "loadRtspStream...cameraConfig:$cameraConfig")
        cameraConfig?.let {
            val empty = it.username.isNotEmpty()
        }
        // 采用安全调用操作符?.来检查cameraConfig是否为null。若不为null,则执行let块内的代码
        cameraConfig?.let {
            // config是使用显式参数名,此处也可用用it来替换config,通过 it 访问 cameraConfig 对象的属性。
            // 在 lambda 表达式中,若只有一个参数,使用 it 可以避免显式地声明参数名,从而使代码更加简洁。
            // 因为 let 函数接收一个 lambda 表达式作为参数,而这个 lambda 表达式只接收一个参数,所以 Kotlin 自动用 it 来指代这个参数,也就是 cameraConfig 的值。
            config ->
            showLoadingView(true)

            try {
                val uri = if (config.username.isNotEmpty() && config.password.isNotEmpty()) {
                    // 把config.rtspUrl字符串解析成Uri对象
                    val originalUri = Uri.parse(config.rtspUrl)
                    Log.v(TAG, "loadRtspStream...rtspUrl:${config.rtspUrl}")
                    Log.v(TAG, "loadRtspStream...originalUri:$originalUri")
                    val authority = "${config.username}:${config.password}@${originalUri.host}:${originalUri.port}"
                    Log.v(TAG, "loadRtspStream...authority:$authority")
                    // 在原始的Uri对象基础上构建一个新的Uri对象,并且添加身份验证信息到 URL 中
                    originalUri.buildUpon().encodedAuthority(authority).build()
                } else {
                    Uri.parse(config.rtspUrl)
                }

                // 创建 RTSP 媒体源
                // RtspMediaSource.Factory()是创建一个RtspMediaSource的工厂对象
                val mediaSourceFactory = RtspMediaSource.Factory()
                    // 运用apply函数对工厂对象进行配置
                    .apply {
                        if (config.useTcp) {
                            // 强制使用 TCP 模式可以提高某些网络环境下的稳定性
                            setForceUseRtpTcp(true)
                        }
                        // 设置更长的超时时间以应对网络不稳定情况
                        setTimeoutMs(8000)
                    }
                Log.v(TAG, "loadRtspStream...mediaSourceFactory:${mediaSourceFactory}")

                // 利用媒体源工厂创建一个MediaSource对象,该对象的媒体项是从之前构建的Uri对象创建而来。
                val mediaSource = mediaSourceFactory.createMediaSource(MediaItem.fromUri(uri))
                Log.v(TAG, "loadRtspStream...mediaSource:${mediaSource}")

                // 准备播放
                player?.setMediaSource(mediaSource)
                player?.prepare()

                Log.v(CameraViewerActivity.TAG,"开始播放相机: ${config.name}, URL: ${config.rtspUrl}")
            } catch (e: Exception) {
                showErrorView("初始化播放器失败: ${e.message}")
                Log.v(CameraViewerActivity.TAG,"RTSP播放器初始化失败", e)
            }
        }
    }

    private fun getCameras(): List<EricRtspCameraConfig> {
        Log.v(TAG, "getCameras...")
        val camerasJson = sharedPreferences.getString(KEY_CAMERAS, "[]")
        Log.v(TAG, "getCameras...camerasJson:$camerasJson")
        return try {
            Json.decodeFromString(camerasJson!!)
        } catch (e: Exception) {
            emptyList()
        }
    }

    private fun getCamera(cameraId: String): EricRtspCameraConfig? {
        return getCameras().find { it.id == cameraId }
    }

    private fun setCamera() {
        Log.v(TAG, "setCamera...")
        Log.v(TAG, "setCamera...cameraConfig:${cameraConfig}")
        Log.v(TAG, "setCamera...cameraConfig.id:${cameraConfig?.id}")
        val ericCamera = EricRtspCameraConfig(
            id = cameraConfig?.id ?: UUID.randomUUID().toString(),
            name = "eric",
            rtspUrl = "rtsp://ipAddress:port/rtp/*****",
            username = "eric",
            password = "",
            useTcp = true // 默认使用TCP更稳定
        )
        cameraId = ericCamera.id
        saveCamera(ericCamera)
    }

    private fun saveCamera(camera: EricRtspCameraConfig) {
        Log.v(TAG, "saveCamera...")
        Log.v(TAG, "saveCamera...camera.id:${camera.id}")
        val cameras = getCameras().toMutableList()
        Log.v(TAG, "saveCamera...cameras:$cameras")
        val index = cameras.indexOfFirst {
            Log.v(TAG, "saveCamera...camera.id:${camera.id},it.id:${it.id}")
            it.id == camera.id
        }
        Log.v(TAG, "saveCamera...index:${index}")
        if (index != -1) {
            cameras[index] = camera
        } else {
            cameras.add(camera)
        }
        saveCameras(cameras)
    }

    private fun saveCameras(cameras: List<EricRtspCameraConfig>) {
        val camerasJson = Json.encodeToString(cameras)
        Log.v(TAG, "saveCameras:${camerasJson}")
        sharedPreferences.edit().putString(KEY_CAMERAS, camerasJson).apply()
    }

}

4,采用MediaPlayer也可用实现rtsp数据流的播放。

// android.annotation.SuppressLint:用于抑制编译器的某些警告。
import android.annotation.SuppressLint
import android.media.MediaPlayer
import android.os.Bundle
// SurfaceView 用于显示视频画面,SurfaceHolder 用于管理 SurfaceView 的表面。
import android.view.SurfaceHolder
import android.view.SurfaceView
import androidx.appcompat.app.AppCompatActivity
import com.ark.smartpanel.R
import java.io.IOException

class EricRtspVideoPlayerActivity: AppCompatActivity() {
    private lateinit var mediaPlayer: MediaPlayer
    private lateinit var surfaceView: SurfaceView
    private lateinit var surfaceHolder: SurfaceHolder

    @SuppressLint("MissingInflatedId")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.eric_rtsp_video_player)

        // 通过 findViewById 方法找到布局文件中的 SurfaceView 控件。
        surfaceView = findViewById(R.id.surfaceView)
        // 获取 SurfaceView 的 SurfaceHolder 对象。
        surfaceHolder = surfaceView.holder
        // 为 SurfaceHolder 添加一个回调接口 SurfaceCallback,用于监听 SurfaceView 的状态变化。
        surfaceHolder.addCallback(SurfaceCallback())

        mediaPlayer = MediaPlayer()
    }

    // 定义了一个内部类 SurfaceCallback,实现了 SurfaceHolder.Callback 接口,用于监听 SurfaceView 的状态变化。
    private inner class SurfaceCallback : SurfaceHolder.Callback {
        // surfaceCreated 方法在 SurfaceView 的表面创建完成后调用
        override fun surfaceCreated(holder: SurfaceHolder) {
            try {
                // 替换为实际的 RTSP 地址
                val rtspUrl = "rtsp://......"
                // 设置 MediaPlayer 的数据源为 RTSP 地址
                mediaPlayer.setDataSource(rtspUrl)
                // 将 MediaPlayer 的显示表面设置为 SurfaceHolder
                mediaPlayer.setDisplay(holder)
                // 异步准备 MediaPlayer,以便开始播放视频
                mediaPlayer.prepareAsync()
                // 设置 MediaPlayer 的准备完成监听器,当准备完成后开始播放视频。
                mediaPlayer.setOnPreparedListener {
                    it.start()
                }
                // 设置 MediaPlayer 的错误监听器,当出现错误时进行处理。
                mediaPlayer.setOnErrorListener { _, what, extra ->
                    // 处理错误
                    false
                }

            } catch (e: IOException) {
                e.printStackTrace()
            }
        }
        // surfaceChanged 方法在 SurfaceView 的表面大小、格式等发生变化时调用
        override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {

        }
        // surfaceDestroyed 方法在 SurfaceView 的表面被销毁时调用,停止并释放 MediaPlayer 对象。
        override fun surfaceDestroyed(holder: SurfaceHolder) {
            mediaPlayer.stop()
            mediaPlayer.release()
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        if (::mediaPlayer.isInitialized) {
            mediaPlayer.stop()
            mediaPlayer.release()
        }
    }
}