Android RTMP直播练习实践
前言:本文只是练习,本文只是练习,本文只是练习!
直播的核心就是推流和拉流,我们就以RTMP的协议来实现下推流和拉流,其他的协议等我学习后再来补充
1.推流
1.1搭建流媒体服务器,具体搭建方法请参照Windows搭建RTMP服务器_rtmp服务器搭建-CSDN博客
一些软件需要搭梯子,如果没法下载的,可以联系下我,我私发,搭建好后,在浏览器里输入http://localhost:9091/stat
出现该界面,说明搭建成功
1.2推流,上面的搭建文章,有OBS推流和ffmpeg推流,这些推流是软件操作和命令行操作,在Andorid客户端不方便,我们这里采用WangShuo1143368701/WSLiveDemo: 音视频,直播SDK,rtmp推流,录制视频,滤镜。百万用户,线上迭代半年,已经稳定。这个库来进行推流
1.2.1首先build.gradle里进行依赖
implementation 'com.github.WangShuo1143368701:WSLiveDemo:v1.7'
1.2.2进行ndk配置
defaultConfig {
applicationId "com.anssy.videolive"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0"
ndk {
// 设置支持的SO库架构(开发者可以根据需要,选择一个或多个平台的so)
abiFilters "armeabi", "armeabi-v7a", "arm64-v8a", "x86", "arm64-v8a", "x86_64"
}
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
1.2.3 接下来,就是一些权限的申请,布局的编写,和对应代码的实现了,申请的权限是Camera和Record Audio
这里引入一个常用的权限框架
implementation 'com.github.getActivity:XXPermissions:20.0'
具体实现代码:
activity_live_video.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<me.lake.librestreaming.ws.StreamLiveCameraView
android:id="@+id/stream_previewView"
android:layout_width="match_parent"
android:layout_above="@id/bottom_layout"
android:layout_height="match_parent"/>
<LinearLayout
android:layout_width="match_parent"
android:orientation="horizontal"
android:id="@+id/bottom_layout"
android:layout_alignParentBottom="true"
android:layout_height="wrap_content">
<Button
android:layout_width="0dp"
android:text="开始推流"
android:id="@+id/btn_startStreaming"
android:layout_height="wrap_content"
android:layout_weight="1"
/>
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="停止推流"
android:id="@+id/btn_stopStreaming"
/>
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/watch_live"
android:text="查看直播"
/>
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/btn_startRecord"
android:text="开始录制"
/>
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/btn_stopRecord"
android:text="停止录制"
/>
</LinearLayout>
</RelativeLayout>
MainActivity.kt 这里有个注意点,就是这个rtmp的地址,自己搞了半天,这个地址是你本地的IP+在niginx里config文件里面配置的端口号,然后后面的程序名称默认是live,也可以是你配置的程序,具体看画红框的部分
package com.anssy.videolive.ui
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.View.OnClickListener
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.anssy.videolive.R
import com.anssy.videolive.databinding.ActivityLiveVideoBinding
import com.hjq.permissions.OnPermissionCallback
import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions
import jp.co.cyberagent.android.gpuimage.GPUImageAddBlendFilter
import me.lake.librestreaming.core.listener.RESConnectionListener
import me.lake.librestreaming.filter.hardvideofilter.BaseHardVideoFilter
import me.lake.librestreaming.filter.hardvideofilter.HardVideoGroupFilter
import me.lake.librestreaming.ws.StreamAVOption
import me.lake.librestreaming.ws.StreamLiveCameraView
import me.lake.librestreaming.ws.filter.hardfilter.GPUImageBeautyFilter
import me.lake.librestreaming.ws.filter.hardfilter.extra.GPUImageCompatibleFilter
import java.util.LinkedList
/**
* @Description rtmp的直播练习
* @Author yulu
* @CreateTime 2025年01月21日 09:14:03
*/
class MainActivity :AppCompatActivity(),RESConnectionListener,OnClickListener{
private lateinit var mLiveCameraView: StreamLiveCameraView
private lateinit var streamAVOption: StreamAVOption
private val rtmpUrl = "rtmp://192.168.0.209:1935/hls/"
private lateinit var mMainViewBinding:ActivityLiveVideoBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mMainViewBinding = ActivityLiveVideoBinding.inflate(layoutInflater)
setContentView(mMainViewBinding.root)
initView()
requestPermission()
}
/**
* 请求权限
*/
private fun requestPermission() {
val permissionList: MutableList<String> = ArrayList()
permissionList.add(Permission.CAMERA)
permissionList.add(Permission.RECORD_AUDIO)
XXPermissions.with(this)
.permission(permissionList)
.request(object : OnPermissionCallback {
override fun onGranted(permissions: List<String>, all: Boolean) {
if (all) {
initConfig()
}
}
override fun onDenied(permissions: List<String>, never: Boolean) {
}
})
}
private fun initView(){
mLiveCameraView = mMainViewBinding.streamPreviewView
mMainViewBinding.btnStartStreaming.setOnClickListener(this)
mMainViewBinding.btnStopStreaming.setOnClickListener(this)
mMainViewBinding.btnStartRecord.setOnClickListener(this)
mMainViewBinding.btnStopRecord.setOnClickListener(this)
mMainViewBinding.watchLive.setOnClickListener(this)
}
/**
* 进行相关配置
*/
private fun initConfig() {
//参数配置 start
streamAVOption = StreamAVOption()
streamAVOption.streamUrl = rtmpUrl
//参数配置 end
mLiveCameraView.init(this, streamAVOption)
mLiveCameraView.addStreamStateListener(this)
//设置滤镜组
val files = LinkedList<BaseHardVideoFilter>()
files.add(GPUImageCompatibleFilter(GPUImageBeautyFilter()))
files.add(GPUImageCompatibleFilter(GPUImageAddBlendFilter()))
mLiveCameraView.setHardVideoFilter(HardVideoGroupFilter(files))
}
override fun onDestroy() {
super.onDestroy()
mLiveCameraView.destroy()
}
override fun onOpenConnectionResult(result: Int) {
//result 0成功 1 失败
runOnUiThread {
Toast.makeText(
this,
"打开推流连接 状态:$result 推流地址:$rtmpUrl", Toast.LENGTH_LONG
).show()
}
}
override fun onWriteError(result: Int) {
runOnUiThread {
Toast.makeText(
this,
"推流出错,请尝试重连",
Toast.LENGTH_LONG
).show()
}
}
override fun onCloseConnectionResult(result: Int) {
runOnUiThread {
Toast.makeText(
this,
"关闭推流连接 状态:$result",
Toast.LENGTH_LONG
).show()
}
}
override fun onClick(v: View) {
when(v.id){
//开始推流
R.id.btn_startStreaming-> {
if (!mLiveCameraView.isStreaming) {
mLiveCameraView.startStreaming(rtmpUrl)
} else {
Toast.makeText(this, "未打开", Toast.LENGTH_SHORT).show()
}
}
//结束推流
R.id.btn_stopStreaming->{
if (mLiveCameraView.isStreaming) {
mLiveCameraView.stopStreaming()
}
}
//查看直播
R.id.watch_live->{
val intent = Intent(this,VideoLiveActivity::class.java)
intent.putExtra("url",this.rtmpUrl)
startActivity(intent)
}
//开始录制
R.id.btn_startRecord->{
if (!mLiveCameraView.isRecord) {
mLiveCameraView.startRecord()
}
}
//结束录制
R.id.btn_stopRecord->{
if (mLiveCameraView.isRecord) {
mLiveCameraView.stopRecord()
}
}
}
}
}
弄完后,就可以推流了,推流成功的效果如下
2.拉流
2.1 VLC播放器播放,在VLC播放器中输入推流的地址
点击媒体->打开网络串流->输入地址
rtmp://192.168.0.209:1935/hls/
2.2使用IJK内核的播放器来播放该地址,这里引入一个第三方库
Doikki/DKVideoPlayer: Android Video Player. 安卓视频播放器,封装MediaPlayer、ExoPlayer、IjkPlayer。模仿抖音并实现预加载,列表播放,悬浮播放,广告播放,弹幕,视频水印,视频滤镜
build.gradle中依赖
implementation 'xyz.doikki.android.dkplayer:dkplayer-java:3.3.7'
implementation 'xyz.doikki.android.dkplayer:player-ijk:3.3.7'
implementation 'xyz.doikki.android.dkplayer:dkplayer-ui:3.3.7'
application中初始化
package com.anssy.videolive.base
import android.app.Application
import xyz.doikki.videoplayer.ijk.IjkPlayerFactory
import xyz.doikki.videoplayer.player.VideoViewConfig
import xyz.doikki.videoplayer.player.VideoViewManager
/**
* @Description TODO
* @Author yulu
* @CreateTime 2025年01月21日 09:44:15
*/
class BaseApplication : Application() {
override fun onCreate() {
super.onCreate()
VideoViewManager.setConfig(
VideoViewConfig.newBuilder()
.setPlayerFactory(IjkPlayerFactory.create())//使用使用IjkPlayer解码 直播使用
.build()
)
}
}
activity_video_play.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:background="@color/black"
android:layout_height="match_parent">
<xyz.doikki.videoplayer.player.VideoView
android:id="@+id/player"
android:layout_centerInParent="true"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:layout_width="45dp"
android:layout_height="45dp"
android:onClick="back"
android:id="@+id/back_layout"
android:orientation="horizontal"
tools:ignore="UsingOnClickInXml">
<ImageView
android:id="@+id/img_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:contentDescription="@null"
android:scaleType="centerInside"
android:src="@drawable/back_sting_white" />
</LinearLayout>
</RelativeLayout>
VideoLiveActivity.kt
package com.anssy.videolive.ui
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.anssy.videolive.databinding.ActivityVideoPlayBinding
import xyz.doikki.videocontroller.StandardVideoController
/**
* @Description 用于直播播放的控制器
* @Author yulu
* @CreateTime 2025年01月21日 09:50:41
*/
class VideoLiveActivity : AppCompatActivity() {
private lateinit var mVideoLiveVideoBinding: ActivityVideoPlayBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mVideoLiveVideoBinding = ActivityVideoPlayBinding.inflate(layoutInflater)
setContentView(mVideoLiveVideoBinding.root)
initView()
}
private fun initView() {
mVideoLiveVideoBinding.player.setUrl(if (intent.getStringExtra("url")==null) "rtmp://192.168.0.209:1935/hls/" else intent.getStringExtra("url")) //设置视频地址
val controller = StandardVideoController(this)
controller.addDefaultControlComponent("", false)
mVideoLiveVideoBinding.player.setVideoController(controller) //设置控制器
mVideoLiveVideoBinding.player.start() //开始播放,不调用则不自动播放
}
public fun back(view:View){
finish()
}
override fun onPause() {
super.onPause()
mVideoLiveVideoBinding.player.pause()
}
override fun onResume() {
super.onResume()
mVideoLiveVideoBinding.player.resume()
}
override fun onDestroy() {
super.onDestroy()
mVideoLiveVideoBinding.player.release()
}
override fun onBackPressed() {
if (!mVideoLiveVideoBinding.player.onBackPressed()) {
super.onBackPressed()
}
}
}
播放效果:
20250121_112120
源码下载地址:rtmp练习,kotlin资源-CSDN文库