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文库

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/957552.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【算法】集合List和队列

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯 你们的点赞收藏是我前进最大的动力&#xff01;&#xff01; 希望本文内容能够帮助到你&#xff01;&#xff01; 目录 零&#xff1a;集合&#xff0c;队列的用法 一&#xff1a;字母异位词分组 二&#xff1a;二叉树的锯…

一篇文章学会Milvus【Docker 中运行 Milvus(Windows),Python实现对Milvus的操作,源代码案例,已经解决巨坑】【程序员猫爪】

一篇文章学会Milvus【Docker 中运行 Milvus&#xff08;Windows&#xff09;&#xff0c;Python实现对Milvus的操作&#xff0c;源代码案例&#xff0c;已经解决巨坑】【程序员猫爪】 一、Milvus 是什么&#xff1f;【程序员猫爪】1、Milvus 是一种高性能、高扩展性的向量数据库…

第35天:安全开发-JavaEE应用原生反序列化重写方法链条分析触发类类加载

时间轴&#xff1a; 序列化与反序列化图解&#xff1a; 演示案例&#xff1a; Java-原生使用-序列化&反序列化 Java-安全问题-重写方法&触发方法 Java-安全问题-可控其他类重写方法 Java-原生使用-序列化&反序列化 1.为什么进行序列化和反序列化&#xff1…

Python----Python高级(文件操作open,os模块对于文件操作,shutil模块 )

一、文件处理 1.1、文件操作的重要性和应用场景 1.1.1、重要性 数据持久化&#xff1a; 文件是存储数据的一种非常基本且重要的方式。通过文件&#xff0c;我们可 以将程序运行时产生的数据永久保存下来&#xff0c;以便将来使用。 跨平台兼容性&#xff1a; 文件是一种通用…

[MCAL]Mcu配置

PostBuild: PreCompile: 选择时钟来源&#xff1b; 选择初始McuInitClock() 函数 电路手册里有晶振频率&#xff0c;如上所示&#xff1b;

(k8s)k8s部署mysql与redis(无坑版)

0.准备工作 在开始之前&#xff0c;要确保我们的节点已经加入网络并且已经准备好&#xff0c;如果没有可以去看我前面发表的踩坑与解决的文章&#xff0c;希望能够帮到你。 1.k8s部署redis 1.1目标 由于我们的服务器资源较小&#xff0c;所以决定只部署一个redis副本&#x…

Python新春烟花

目录 系列文章 写在前面 技术需求 完整代码 下载代码 代码分析 1. 程序初始化与显示设置 2. 烟花类 (Firework) 3. 粒子类 (Particle) 4. 痕迹类 (Trail) 5. 烟花更新与显示 6. 主函数 (fire) 7. 游戏循环 8. 总结 注意事项 写在后面 系列文章 序号直达链接爱…

机器学习09-Pytorch功能拆解

机器学习09-Pytorch功能拆解 我个人是Java程序员&#xff0c;关于Python代码的使用过程中的相关代码事项&#xff0c;在此进行记录 文章目录 机器学习09-Pytorch功能拆解1-核心逻辑脉络2-个人备注3-Pytorch软件包拆解1-Python有参和无参构造构造方法的基本语法示例解释注意事项…

AI在SEO中的关键词优化策略探讨

内容概要 在当今数字化时代&#xff0c;人工智能&#xff08;AI&#xff09;正逐渐重塑搜索引擎优化&#xff08;SEO&#xff09;行业。AI技术的快速发展使得SEO策略发生了翻天覆地的变化&#xff0c;特别是在关键词优化方面。关键词优化的基本概念是通过选择与用户搜索意图密…

探索与创作:2024年我在CSDN平台上的成长与突破

文章目录 我与CSDN的初次邂逅初学阶段的阅读CSDN&#xff1a;编程新手的避风港初学者的福音&#xff1a;细致入微的知识讲解考试复习神器&#xff1a;技术总结的“救命指南”曾经的自己&#xff1a;为何迟迟不迈出写博客的第一步兴趣萌芽&#xff1a;从“读”到“想写”的初体验…

element el-table合并单元格

合并 表格el-table添加方法:span-method"” <el-table v-loading"listLoading" :data"SHlist" ref"tableList" element-loading-text"Loading" border fit highlight-current-row :header-cell-style"headClass" …

行业热点丨低空飞行eVTOL的关键技术与发展趋势

本篇主要围绕eVTOL仿真难点和趋势&#xff0c;eVTOL仿真多学科解决方案和当下热门的AI或者机器学习的方法在EVTOL中的应用展开。 eVTOL 研发难点 首先是eVTOL研发难点&#xff0c;区别于上个世纪70年代就已经构型稳定或者技术方法稳定的民航客机&#xff0c;eVTOL到今天尚未有经…

BOBO小火炬全套源码XE修复版2025(火炬天花板二次开发版)

《小火炬全套源码 传奇游戏源码讲解》 小火炬全套源码是一种用于开发经典传奇类游戏的源码包。传奇游戏作为一款经典的多人在线角色扮演游戏&#xff08;MMORPG&#xff09;&#xff0c;有着庞大的用户基础和强大的游戏生态。小火炬全套源码主要提供了从基础架构到核心功能的完…

Flutter:搜索页,搜索bar封装

view 使用内置的Chip简化布局 import package:chenyanzhenxuan/common/index.dart; import package:ducafe_ui_core/ducafe_ui_core.dart; import package:flutter/material.dart; import package:get/get.dart; import package:tdesign_flutter/tdesign_flutter.dart;import i…

网络通信---MCU移植LWIP

使用的MCU型号为STM32F429IGT6&#xff0c;PHY为LAN7820A 目标是通过MCU的ETH给LWIP提供输入输出从而实现基本的Ping应答 OK废话不多说我们直接开始 下载源码 LWIP包源码&#xff1a;lwip源码 -在这里下载 ST官方支持的ETH包&#xff1a;ST-ETH支持包 这里下载 创建工程 …

麒麟操作系统服务架构保姆级教程(十三)tomcat环境安装以及LNMT架构

如果你想拥有你从未拥有过的东西&#xff0c;那么你必须去做你从未做过的事情 之前咱们学习了LNMP架构&#xff0c;但是PHP对于技术来说确实是老掉牙了&#xff0c;PHP的市场占有量越来越少了&#xff0c;我认识一个10年的PHP开发工程师&#xff0c;十年工资从15k到今天的6k&am…

elementUI Table组件实现表头吸顶效果

需求描述 当 table 内容过多的时候&#xff0c;页面上滑滚动&#xff0c;表头的信息也会随着被遮挡&#xff0c;无法将表头信息和表格内容对应起来&#xff0c;需要进行表头吸顶 开始编码&#x1f4aa; 环境&#xff1a;vue2.6、element UI step1&#xff1a; 给el-table__h…

AI 新动态:技术突破与应用拓展

目录 一.大语言模型的持续进化 二.AI 在医疗领域的深度应用 疾病诊断 药物研发 三.AI 与自动驾驶的新进展 四.AI 助力环境保护 应对气候变化 能源管理 后记 在当下科技迅猛发展的时代&#xff0c;人工智能&#xff08;AI&#xff09;无疑是最具影响力的领域之一。AI 技…

题解 CodeForces 131D Subway BFS C++

题目传送门 Problem - 131D - Codeforceshttps://codeforces.com/problemset/problem/131/Dhttps://codeforces.com/problemset/problem/131/D 翻译 地铁方案&#xff0c;对于Berland城市来说是一种经典的表示&#xff0c;由一组n站点和连接这些站点的n通道组成&#xff0c;…

如何查看某用户的Git提交数

说明&#xff1a;有些公司自己搭建的Git仓库&#xff0c;可以在仓库项目上查看各用户的提交量及占比。也可通过下面这两个Git命令&#xff0c;查看当前仓库&#xff0c;当前分支的总提交数&#xff0c;及某用户的提交数&#xff1b; # 当前分支的总提交数 git log --oneline |…