Android现场流
#android #developers #livestream

如果您是在线内容的狂热消费者,您通常会消耗大量的实时流。随着直播成为最喜欢的学习和娱乐来源,很难错过现场广播。无论是参加体育赛事,参加在线课程,观看健身课程还是与名人互动。

许多实时流媒体应用程序都依赖于HTTP实时流媒体(HLS - 一种广泛使用的协议)来向受众传递内容。实际上,如果您曾经观看过Instagram直播,那么您已经有了HLS的魔力。

如果您是一个希望在Android应用中构建一流的直播体验的开发人员,则本文适合您。

为什么选择视频SDK?

视频SDK是那些寻求实时流媒体平台的人的理想选择,该平台提供了创建高质量流的功能。该平台支持各种功能,例如屏幕共享,实时消息传递,允许广播公司邀请观众进入舞台并支持100名参与者,从而确保您的实时流互动和引人入胜。使用Video SDK,您还可以使用自己的自定义设计布局模板进行实时流媒体。

在集成方面,视频SDK非常简单且快速地集成,这使您可以无缝地集成在应用中的实时流。这样可以确保您可以在没有任何技术困难或冗长的实施过程的情况下享受现场流的好处。

此外,Video SDK对预算友好,使其成为各种规模的企业的负担得起的选择。您可以享受功能丰富的实时流媒体平台的好处,而不会破坏银行,这是初创企业和小型企业的理想选择。

构建现场流媒体应用程序

以下步骤将为您提供所有信息,以快速构建交互式实时流媒体应用程序。请仔细跟随。有麻烦吗?让我们立即在不和谐上知道,我们很乐意为您提供帮助。

先决条件

  • Java开发套件
  • Android Studio 3.0或更高版本。
  • Android SDK API级别21或更高。
  • 运行Android 5.0或更高版本的移动设备。
  • 视频SDK仪表板的令牌

设置项目

创建新项目

  • 在Android Studio中,创建一个具有空活动的电话和平板电脑项目。

  • 下一步是提供一个名称。我们将名称设置为HLSDEMO。

集成视频SDK

  • 将存储库添加到Project的设置.gradle文件中。
dependencyResolutionManagement{
  repositories {
    // ...
    google()
    mavenCentral()
    maven { url 'https://jitpack.io' }
    maven { url "https://maven.aliyun.com/repository/jcenter" }
  }
}
  • 在您的应用程序build.gradle中添加以下依赖关系。
dependencies {
  implementation 'live.videosdk:rtc-android-sdk:0.1.15'
  // library to perform Network call to generate a meeting id
  implementation 'com.amitshekhar.android:android-networking:1.0.2'
  // other app dependencies
  }

如果您的项目已设置android.useandroidx = true,则在gradle.properties文件中设置android.enableJetifier = true。将您的项目迁移到Androidx并避免重复的类别冲突。

将权限添加到您的项目中

/app/Manifests/AndroidManifest.xml中,添加</application>之后的以下权限。

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />

项目的结构

我们将创建两个活动和两个片段。第一个活动是JoinActivityâ,它允许用户创建/加入会议,另一个是MeetingActivityâ,它将根据用户的选择初始化会议并用SpeakerFragmentViewerFragment替换mainLayout

我们的项目结构看起来像这样。

app
   ├── java
       ├── packagename
            ├── JoinActivity
            ├── MeetingActivity
            ├── SpeakerAdapter
            ├── SpeakerFragment
   |         ├── ViewerFragment
   ├── res
       ├── layout
           ├── activity_join.xml
           ├── activity_meeting.xml
   |    |    ├── fragment_speaker.xml
   |    |    ├── fragment_viewer.xml
           ├── item_remote_peer.xmlCopy

您必须将加入性设置为发射器活动。

应用结构

步骤1:创建加入屏幕

创建一个名为JoinActivity

的新活动

创建UI以加入屏幕

连接屏幕将包括:

  1. 创建按钮 - 此按钮将为您创建新的会议。
  2. textfield进行会议IDâââââ€将包含您要加入的会议ID。
  3. 加入主机按钮 - 此按钮将与您提供的meetingId一起加入会议。
  4. 加入查看器按钮 - 此按钮将与您提供的meetingId一起参加会议。

/app/res/layout/activity_join.xml文件中,用以下内容替换内容。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/createorjoinlayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/black"
    android:gravity="center"
    android:orientation="vertical">

    <Button
        android:id="@+id/btnCreateMeeting"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Create Meeting"
        android:textAllCaps="false" />

    <TextView
        android:id="@+id/tvText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingVertical="5sp"
        android:text="OR"
        android:textColor="@color/white"
        android:textSize="20sp" />

    <EditText
        android:id="@+id/etMeetingId"
        android:theme="@android:style/Theme.Holo"
        android:layout_width="250dp"
        android:layout_height="wrap_content"
        android:hint="Enter Meeting Id"
        android:textColor="@color/white"
        android:textColorHint="@color/white" />

    <Button
        android:id="@+id/btnJoinHostMeeting"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8sp"
        android:text="Join as Host"
        android:textAllCaps="false" />

    <Button
        android:id="@+id/btnJoinViewerMeeting"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Join as Viewer"
        android:textAllCaps="false" />

</LinearLayout>

集成创建会议API

  1. JoinActivity中创建field sampleToken,该field11将在VideoSDK dashboard中保存生成的令牌。该令牌将用于VideoSDK配置以及生成METICID。
class JoinActivity : AppCompatActivity() {

  //Replace with the token you generated from the VideoSDK Dashboard
  private var sampleToken = ""

  override fun onCreate(savedInstanceState: Bundle?) {
    //...
  }
}
  1. 在JOIN按钮作为主持人onclick事件上,我们将Naviagte与令牌,会议和模式作为会议。

  2. 在加入按钮上作为观看器onClick事件,我们将Naviagte与令牌,MeetingID和Mode作为查看器的会议。

class JoinActivity : AppCompatActivity() {

   //Replace with the token you generated from the VideoSDK Dashboard
   private var sampleToken = "" 

   override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_join)

      val btnCreate = findViewById<Button>(R.id.btnCreateMeeting)
      val btnJoinHost = findViewById<Button>(R.id.btnJoinHostMeeting)
      val btnJoinViewer = findViewById<Button>(R.id.btnJoinViewerMeeting)
      val etMeetingId = findViewById<EditText>(R.id.etMeetingId)

      // create meeting and join as Host
      btnCreate.setOnClickListener {
          createMeeting(
              sampleToken
          )
      }

      // Join as Host
      btnJoinHost.setOnClickListener {
          val intent = Intent(this@JoinActivity, MeetingActivity::class.java)
          intent.putExtra("token", sampleToken)
          intent.putExtra("meetingId", etMeetingId.text.toString().trim { it <= ' ' })
          intent.putExtra("mode", "CONFERENCE")
          startActivity(intent)
      }

      // Join as Viewer
      btnJoinViewer.setOnClickListener {
          val intent = Intent(this@JoinActivity, MeetingActivity::class.java)
          intent.putExtra("token", sampleToken)
          intent.putExtra("meetingId", etMeetingId.text.toString().trim { it <= ' ' })
          intent.putExtra("mode", "VIEWER")
          startActivity(intent)
      }
    }

    private fun createMeeting(token: String) {
      // we will explore this method in the next step
    }
  1. 对于创建按钮,在创建方法下,我们将通过调用API并导航以与令牌,生成的会议ID和模式作为会议来生成oferitid。
class JoinActivity : AppCompatActivity() {
  //...onCreate
 private fun createMeeting(token: String) {
  // we will make an API call to VideoSDK Server to get a roomId
  AndroidNetworking.post("https://api.videosdk.live/v2/rooms")
      .addHeaders("Authorization", token) //we will pass the token in the Headers
      .build()
      .getAsJSONObject(object : JSONObjectRequestListener {
          override fun onResponse(response: JSONObject) {
            try {
              // response will contain `roomId`
              val meetingId = response.getString("roomId")

              // starting the MeetingActivity with received roomId and our sampleToken
              val intent = Intent(this@JoinActivity, MeetingActivity::class.java)
              intent.putExtra("token", sampleToken)
              intent.putExtra("meetingId", meetingId)
              intent.putExtra("mode", "CONFERENCE")
              startActivity(intent)
            } catch (e: JSONException) {
                e.printStackTrace()
            }
          }

          override fun onError(anError: ANError) {
            anError.printStackTrace()
            Toast.makeText(this@JoinActivity, anError.message, Toast.LENGTH_SHORT)
                .show()
          }
      })
  }
}
  1. 我们的应用程序完全基于音频和视频通勤,这就是为什么我们需要要求运行时许可RECORD_AUDIOCAMERA。因此,我们将在JoinActivity上实现权限逻辑。
class JoinActivity : AppCompatActivity() {
  companion object {
    private const val PERMISSION_REQ_ID = 22
    private val REQUESTED_PERMISSIONS = arrayOf(
        Manifest.permission.RECORD_AUDIO,
        Manifest.permission.CAMERA
    )
  }

  private fun checkSelfPermission(permission: String, requestCode: Int) {
    if (ContextCompat.checkSelfPermission(this, permission) !=
        PackageManager.PERMISSION_GRANTED
    ) {
        ActivityCompat.requestPermissions(this, REQUESTED_PERMISSIONS, requestCode)
    }
  }

  override fun onCreate(savedInstanceState: Bundle?) {
    //... button listeneres
    checkSelfPermission(REQUESTED_PERMISSIONS[0], PERMISSION_REQ_ID)
    checkSelfPermission(REQUESTED_PERMISSIONS[1], PERMISSION_REQ_ID)
  }
}

您将获得未解决的参考:会议活动错误,但请不要担心。创建会议之后,它将自动解决。

步骤2:创建会议屏幕

创建一个名为“会议”的新活动。

创建用于会议屏幕的UI

/app/res/layout/activity_meeting.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:id="@+id/mainLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/black"
    tools:context=".MeetingActivity">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="Creating a meeting for you"
        android:textColor="@color/white"
        android:textFontWeight="700"
        android:textSize="20sp" />

</RelativeLayout>

初始化会议

JoinActivity获得令牌后,遇见和模式后,

  1. 初始化videosdk。
  2. 用令牌配置VideoSdk。
  3. 以所需的参数(例如meetingIdparticipantNamemicEnabledwebcamEnabled mode等)初始化会议。
  4. 使用meeting.join()方法加入房间。
  5. 添加MeetingEventListener进行收听会议加入活动。
  6. 检查localParticipant的模式,如果模式是会议,那么我们将用SpeakerFragment替换mainlayout,否则用ViewerFragment替换。
class MeetingActivity : AppCompatActivity() {
  var meeting: Meeting? = null
      private set

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_meeting)

    val meetingId = intent.getStringExtra("meetingId")
    val token = intent.getStringExtra("token")
    val mode = intent.getStringExtra("mode")
    val localParticipantName = "John Doe"
    val streamEnable = mode == "CONFERENCE"

    // initialize VideoSDK
    VideoSDK.initialize(applicationContext)

    // Configuration VideoSDK with Token
    VideoSDK.config(token)

    // Initialize VideoSDK Meeting
    meeting = VideoSDK.initMeeting(
        this@MeetingActivity, meetingId, localParticipantName,
        streamEnable, streamEnable, null, mode, null
    )

    // join Meeting
    meeting!!.join()

    // if mode is CONFERENCE than replace mainLayout with SpeakerFragment otherwise with ViewerFragment
    meeting!!.addEventListener(object : MeetingEventListener() {
      override fun onMeetingJoined() {
          if (meeting != null) {
            if (mode == "CONFERENCE") {
              //pin the local partcipant
              meeting!!.localParticipant.pin("SHARE_AND_CAM")
              supportFragmentManager
                  .beginTransaction()
                  .replace(R.id.mainLayout, SpeakerFragment(), "MainFragment")
                  .commit()
              } else if (mode == "VIEWER") {
                supportFragmentManager
                    .beginTransaction()
                    .replace(R.id.mainLayout, ViewerFragment(), "viewerFragment")
                    .commit()
              }
          }
      }
    })
  }
}

步骤3:实施Speakerview

成功参加会议后,是时候渲染发言人的视图并管理控制诸如切换网络摄像头/麦克风,开始/停止HLS并离开会议了。

  1. 创建一个名为SpeakerFragment的新片段。
  2. /app/res/layout/fragment_speaker.xml文件中,用以下内容替换内容。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/black"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".SpeakerFragment">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginVertical="8sp"
        android:paddingHorizontal="10sp">

        <TextView
            android:id="@+id/tvMeetingId"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="Meeting Id : "
            android:textColor="@color/white"
            android:textSize="18sp"
            android:layout_weight="3"/>

        <Button
            android:id="@+id/btnLeave"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:text="Leave"
            android:textAllCaps="false"
            android:layout_weight="1"/>

    </LinearLayout>

    <TextView
        android:id="@+id/tvHlsState"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Current HLS State : NOT_STARTED"
        android:textColor="@color/white"
        android:textSize="18sp" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rvParticipants"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginVertical="10sp"
        android:layout_weight="1" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center">

        <Button
            android:id="@+id/btnHLS"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Start HLS"
            android:textAllCaps="false" />

        <Button
            android:id="@+id/btnWebcam"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginHorizontal="5sp"
            android:text="Toggle Webcam"
            android:textAllCaps="false" />

        <Button
            android:id="@+id/btnMic"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Toggle Mic"
            android:textAllCaps="false" />

    </LinearLayout>

</LinearLayout>
  1. 现在,让S设置按钮的侦听器,该按钮将允许参与者切换媒体。
class SpeakerFragment : Fragment() {
  private var micEnabled = true
  private var webcamEnabled = true
  private var hlsEnabled = false
  private var btnMic: Button? = null
  private var btnWebcam: Button? = null
  private var btnHls: Button? = null
  private var btnLeave: Button? = null
  private var tvMeetingId: TextView? = null
  private var tvHlsState: TextView? = null
  override fun onAttach(context: Context) {
    super.onAttach(context)
    mContext = context
    if (context is Activity) {
      mActivity = context
      // getting meeting object from Meeting Activity
      meeting = (mActivity as MeetingActivity?)!!.meeting
    }
  }

  override fun onCreateView(
      inflater: LayoutInflater, container: ViewGroup?,
      savedInstanceState: Bundle?
  ): View? {
    // Inflate the layout for this fragment
    val view = inflater.inflate(R.layout.fragment_speaker, container, false)
    btnMic = view.findViewById(R.id.btnMic)
    btnWebcam = view.findViewById(R.id.btnWebcam)
    btnHls = view.findViewById(R.id.btnHLS)
    btnLeave = view.findViewById(R.id.btnLeave)
    tvMeetingId = view.findViewById(R.id.tvMeetingId)
    tvHlsState = view.findViewById(R.id.tvHlsState)
    if (meeting != null) {
      tvMeetingId!!.text = "Meeting Id : " + meeting!!.meetingId
      setActionListeners()
    }
    return view
  }

  private fun setActionListeners() {}

  companion object {
    private var mActivity: Activity? = null
    private var mContext: Context? = null
    private var meeting: Meeting? = null
  }
}

private fun setActionListeners() {
    btnMic!!.setOnClickListener {
      if (micEnabled) {
        meeting!!.muteMic()
        Toast.makeText(mContext, "Mic Muted", Toast.LENGTH_SHORT).show()
      } else {
        meeting!!.unmuteMic()
        Toast.makeText(
            mContext,
            "Mic Enabled",
            Toast.LENGTH_SHORT
        ).show()
      }
      micEnabled = !micEnabled
    }
    btnWebcam!!.setOnClickListener {
      if (webcamEnabled) {
        meeting!!.disableWebcam()
        Toast.makeText(
            mContext,
            "Webcam Disabled",
            Toast.LENGTH_SHORT
        ).show()
      } else {
        meeting!!.enableWebcam()
        Toast.makeText(
            mContext,
            "Webcam Enabled",
            Toast.LENGTH_SHORT
        ).show()
      }
      webcamEnabled = !webcamEnabled
    }
    btnLeave!!.setOnClickListener { meeting!!.leave() }
    btnHls!!.setOnClickListener {
      if (!hlsEnabled) {
        val config = JSONObject()
        val layout = JSONObject()
        JsonUtils.jsonPut(layout, "type", "SPOTLIGHT")
        JsonUtils.jsonPut(layout, "priority", "PIN")
        JsonUtils.jsonPut(layout, "gridSize", 4)
        JsonUtils.jsonPut(config, "layout", layout)
        JsonUtils.jsonPut(config, "orientation", "portrait")
        JsonUtils.jsonPut(config, "theme", "DARK")
        JsonUtils.jsonPut(config, "quality", "high")
        meeting!!.startHls(config)
      } else {
        meeting!!.stopHls()
      }
    }
  }
  1. 在添加按钮的列表之后,让我们在会议中添加ceetingEventListener,然后在ondestroy()方法中删除所有列表。
class SpeakerFragment : Fragment() {

  override fun onCreateView(
      inflater: LayoutInflater, container: ViewGroup?,
      savedInstanceState: Bundle?
  ): View? {
    //...
    if (meeting != null) {
      //...
      // add Listener to the meeting
      meeting!!.addEventListener(meetingEventListener)
    }
    return view
  }

  private val meetingEventListener: MeetingEventListener = object : MeetingEventListener() {
      override fun onMeetingLeft() {
        // unpin the local participant
        meeting!!.localParticipant.unpin("SHARE_AND_CAM")
        if (isAdded) {
          val intents = Intent(mContext, JoinActivity::class.java)
          intents.addFlags(
              Intent.FLAG_ACTIVITY_NEW_TASK
                      or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK
          )
          startActivity(intents)
          mActivity!!.finish()
        }
      }

      @RequiresApi(api = Build.VERSION_CODES.P)
      override fun onHlsStateChanged(HlsState: JSONObject) {
        if (HlsState.has("status")) {
          try {
              tvHlsState!!.text = "Current HLS State : " + HlsState.getString("status")
              if (HlsState.getString("status") == "HLS_STARTED") {
                hlsEnabled = true
                btnHls!!.text = "Stop HLS"
              }
              if (HlsState.getString("status") == "HLS_STOPPED") {
                hlsEnabled = false
                btnHls!!.text = "Start HLS"
              }
            } catch (e: JSONException) {
                e.printStackTrace()
            }
        }
      }
  }

  override fun onDestroy() {
      mContext = null
      mActivity = null
      if (meeting != null) {
          meeting!!.removeAllListeners()
          meeting = null
      }
      super.onDestroy()
  }
}
  1. 下一步是呈现说话者的观点。借助Recyclerview,我们将展示一份参加会议的参与者列表。
  • res/layout folder中的名为item_remote_peer.xml的参与者视图创建一个新的布局。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:background="@color/cardview_dark_background"
    tools:layout_height="200dp">

    <live.videosdk.rtc.android.VideoView
        android:id="@+id/participantView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:background="#99000000"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/tvName"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:padding="4dp"
            android:textColor="@color/white" />

    </LinearLayout>

</FrameLayout>
  • 创建一个名为SpeakerAdapter的回收器视图适配器,该适配器将显示参与者列表。在适配器中创建Peerveiewholder,该适配器将扩展RecyClerview.ViewHolder。
class SpeakerAdapter(private val meeting: Meeting) :
    RecyclerView.Adapter<SpeakerAdapter.PeerViewHolder?>() {
    private var participantList: MutableList<Participant> = ArrayList()

    init {
      updateParticipantList()
      // adding Meeting Event listener to get the participant join/leave event in the meeting.
      meeting.addEventListener(object : MeetingEventListener() {
        override fun onParticipantJoined(participant: Participant) {
          // check participant join as Host/Speaker or not
          if (participant.mode == "CONFERENCE") {
              // pin the participant
              participant.pin("SHARE_AND_CAM")
              // add participant in participantList
              participantList.add(participant)
          }
          notifyDataSetChanged()
        }

        override fun onParticipantLeft(participant: Participant) {
          var pos = -1
          for (i in participantList.indices) {
              if (participantList[i].id == participant.id) {
                  pos = i
                  break
              }
          }
          if (participantList.contains(participant)) {
              // unpin participant who left the meeting
              participant.unpin("SHARE_AND_CAM")
              // remove participant from participantList
              participantList.remove(participant)
          }
          if (pos >= 0) {
              notifyItemRemoved(pos)
          }
        }
      })
    }

    private fun updateParticipantList() {
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PeerViewHolder {
    }

    override fun onBindViewHolder(holder: PeerViewHolder, position: Int) {
    }

    override fun getItemCount(): Int {
    }

    class PeerViewHolder(view: View) : RecyclerView.ViewHolder(view) {
    }
}
private fun updateParticipantList() {
      // adding the local participant(You) to the list
      participantList.add(meeting.localParticipant)

      // adding participants who join as Host/Speaker
      val participants: Iterator<Participant> = meeting.participants.values.iterator()
      for (i in 0 until meeting.participants.size) {
        val participant = participants.next()
        if (participant.mode == "CONFERENCE") {
            // pin the participant
            participant.pin("SHARE_AND_CAM")
            // add participant in participantList
            participantList.add(participant)
        }
      }
    }

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PeerViewHolder {
      return PeerViewHolder(
          LayoutInflater.from(parent.context).inflate(R.layout.item_remote_peer, parent, false)
      )
    }

    override fun onBindViewHolder(holder: PeerViewHolder, position: Int) {
      val participant = participantList[position]
      holder.tvName.text = participant.displayName

      // adding the initial video stream for the participant into the 'VideoView'
      for ((_, stream) in participant.streams) {
        if (stream.kind.equals("video", ignoreCase = true)) {
          holder.participantView.visibility = View.VISIBLE
          val videoTrack = stream.track as VideoTrack
          holder.participantView.addTrack(videoTrack)
          break
        }
      }

      // add Listener to the participant which will update start or stop the video stream of that participant
      participant.addEventListener(object : ParticipantEventListener() {
        override fun onStreamEnabled(stream: Stream) {
          if (stream.kind.equals("video", ignoreCase = true)) {
            holder.participantView.visibility = View.VISIBLE
            val videoTrack = stream.track as VideoTrack
            holder.participantView.addTrack(videoTrack)
          }
        }

        override fun onStreamDisabled(stream: Stream) {
          if (stream.kind.equals("video", ignoreCase = true)) {
            holder.participantView.removeTrack()
            holder.participantView.visibility = View.GONE
          }
        }
      })
    }

    override fun getItemCount(): Int {
      return participantList.size
    }

    class PeerViewHolder(view: View) : RecyclerView.ViewHolder(view) {
      // 'VideoView' to show Video Stream
      var participantView: VideoView
      var tvName: TextView

      init {
        tvName = view.findViewById(R.id.tvName)
        participantView = view.findViewById(R.id.participantView)
      }
    }
  1. 将此适配器添加到SpeakerFragment
override fun onCreateView(
      inflater: LayoutInflater, container: ViewGroup?,
      savedInstanceState: Bundle?
  ): View? {
    //...
    if (meeting != null) {
      //...
      val rvParticipants = view.findViewById<RecyclerView>(R.id.rvParticipants)
      rvParticipants.layoutManager = GridLayoutManager(mContext, 2)
      rvParticipants.adapter = SpeakerAdapter(meeting!!)
  }
}

步骤4:实现Viewerview

当主机开始实时流媒体时,观看者将能够看到实时流。
要实现播放器视图,我们将使用epeplayer。玩HLS流将很有帮助。
让我们首先将依赖性添加到项目中。

dependencies {
  implementation 'com.google.android.exoplayer:exoplayer:2.18.5'
  // other app dependencies
}

创建一个名为vieverfragment的新片段。

为观众片段创建UI

观众片段将包括:

  1. 您加入的会议ID会议ID的文本视图将在此文本视图中显示。
  2. 离开按钮 - 此按钮将离开会议。
  3. 等待LAEDLAYOUT - 这是当没有活动HLS时显示的TextView。
  4. stypledplayerviewâ€这是MediaPlayer,它将展示直播。

/app/res/layout/fragment_viewer.xml文件中,用以下内容替换内容。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/black"
    tools:context=".ViewerFragment">

    <LinearLayout
        android:id="@+id/meetingLayout"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingHorizontal="12sp"
        android:paddingVertical="5sp">

        <TextView
            android:id="@+id/meetingId"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="3"
            android:text="Meeting Id : "
            android:textColor="@color/white"
            android:textSize="20sp" />

        <Button
            android:id="@+id/btnLeave"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Leave" />

    </LinearLayout>

    <TextView
        android:id="@+id/waitingLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="Waiting for host \n to start the live streaming"
        android:textColor="@color/white"
        android:textFontWeight="700"
        android:textSize="20sp"
        android:gravity="center"/>

    <com.google.android.exoplayer2.ui.StyledPlayerView
        android:id="@+id/player_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="gone"
        app:resize_mode="fixed_width"
        app:show_buffering="when_playing"
        app:show_subtitle_button="false"
        app:use_artwork="false"
        app:show_next_button="false"
        app:show_previous_button="false"
        app:use_controller="true"
        android:layout_below="@id/meetingLayout"/>

</RelativeLayout>

初始化播放器并玩HLS流

  1. 初始化播放器并在会议hls状态为hls_plays时播放HLS,并在HLS状态为HLS_STOP时将其发布。每当HLS状态发生变化时,将触发事件Onhlsstatatechang。
class ViewerFragment : Fragment() {
  private var meeting: Meeting? = null
  private var playerView: StyledPlayerView? = null
  private var waitingLayout: TextView? = null
  private var player: ExoPlayer? = null
  private var dataSourceFactory: DefaultHttpDataSource.Factory? = null
  private val startAutoPlay = true
  private var downStreamUrl: String? = ""

  override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
  ): View? {
    // Inflate the layout for this fragment
    val view = inflater.inflate(R.layout.fragment_viewer, container, false)
    playerView = view.findViewById(R.id.player_view)
    waitingLayout = view.findViewById(R.id.waitingLayout)
    if (meeting != null) {
        // set MeetingId to TextView
        (view.findViewById<View>(R.id.meetingId) as TextView).text =
            "Meeting Id : " + meeting!!.meetingId
        // leave the meeting on btnLeave click
        (view.findViewById<View>(R.id.btnLeave) as Button).setOnClickListener { meeting!!.leave() }
        // add listener to meeting
        meeting!!.addEventListener(meetingEventListener)
    }
    return view
  }

  override fun onAttach(context: Context) {
      super.onAttach(context)
      mContext = context
      if (context is Activity) {
        mActivity = context
        // get meeting object from MeetingActivity
        meeting = (mActivity as MeetingActivity?)!!.meeting
      }
  }
  private val meetingEventListener: MeetingEventListener = object : MeetingEventListener() {}

  override fun onDestroy() {
  }

  companion object {
    private var mActivity: Activity? = null
    private var mContext: Context? = null
  }
}

private val meetingEventListener: MeetingEventListener = object : MeetingEventListener() {
      override fun onMeetingLeft() {
        if (isAdded) {
          val intents = Intent(mContext, JoinActivity::class.java)
          intents.addFlags(
              Intent.FLAG_ACTIVITY_NEW_TASK
                      or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK
          )
          startActivity(intents)
          mActivity!!.finish()
        }
      }

      @RequiresApi(api = Build.VERSION_CODES.P)
      override fun onHlsStateChanged(HlsState: JSONObject) {
        if (HlsState.has("status")) {
            try {
              if (HlsState.getString("status") == "HLS_PLAYABLE" && HlsState.has("downstreamUrl")) {
                downStreamUrl = HlsState.getString("downstreamUrl")
                waitingLayout!!.visibility = View.GONE
                playerView!!.visibility = View.VISIBLE
                // initialize player
                initializePlayer()
              }
              if (HlsState.getString("status") == "HLS_STOPPED") {
                // release the player
                releasePlayer()
                downStreamUrl = null
                waitingLayout!!.text = "Host has stopped \n the live streaming"
                waitingLayout!!.visibility = View.VISIBLE
                playerView!!.visibility = View.GONE
              }
            } catch (e: JSONException) {
                e.printStackTrace()
            }
          }
      }
  }
  private fun initializePlayer() {
  }

   private fun releasePlayer() {
   }

private fun initializePlayer() {
    if (player == null) {
      dataSourceFactory = DefaultHttpDataSource.Factory()
      val mediaSource = HlsMediaSource.Factory(dataSourceFactory!!).createMediaSource(
          MediaItem.fromUri(Uri.parse(downStreamUrl))
      )
      val playerBuilder = ExoPlayer.Builder( /* context = */mContext!!)
      player = playerBuilder.build()
      // auto play when player is ready
      player!!.playWhenReady = startAutoPlay
      player!!.setMediaSource(mediaSource)
      // if you want display setting for player then remove this line
      playerView!!.findViewById<View>(com.google.android.exoplayer2.ui.R.id.exo_settings).visibility =
          View.GONE
      playerView!!.player = player
    }
    player!!.prepare()
  }
 private fun releasePlayer() {
    if (player != null) {
      player!!.release()
      player = null
      dataSourceFactory = null
      playerView!!.player = null
    }
  }

  override fun onDestroy() {
    mContext = null
    mActivity = null
    downStreamUrl = null
    releasePlayer()
    if (meeting != null) {
        meeting!!.removeAllListeners()
        meeting = null
    }
    super.onDestroy()
  }

这就是观众看到他们的屏幕的方式。
Output

运行您的应用程序

tadaa!我们的应用已准备好进行直播。容易,不是吗?
在两个不同的设备上安装并运行该应用程序,并确保它们都连接到Internet。

Image description

结论

在此博客中,我们熟悉VideosDK以及如何使用视频SDK创建自己的实时流媒体应用程序。

继续创建高级功能,例如屏幕共享,实时消息和其他功能。浏览我们的documentation

要查看应用程序的完整实现,请查看此GitHub repository

如果您遇到任何问题或有疑问,请随时加入我们的Discord community

更多的Android资源

Build an Android Video Calling App — docs

Build an Android Video Calling App using Android Studio and Video SDK — Youtube

quickstart/android-rtc

quickstart/android-hls

videosdk-rtc-android-java-sdk-example

videosdk-rtc-android-kotlin-sdk-example

videosdk-hls-android-java-example

videosdk-hls-android-kotlin-example