使用Java使用视频SDK构建Interactive Live流媒体应用程序
#java #android #developers

介绍

Interactive Live流媒体已获得极大的知名度,为内容创建者,企业和开发人员提供了独特的机会,可以实时与听众建立联系。

在本教程中,您将学习如何使用Video SDKJavaAndroid应用中集成Interactive Live流。

视频SDK是一种强大的工具,可让您将实时交互式流功能合并到应用程序中。借助Interactive实时流媒体,您可以让用户参与动态和沉浸式的体验,例如现场活动,游戏广播,虚拟教室等。

在本教程结束时,您将获得了将Interactive Live流入到您的Android应用程序中的宝贵技能,使您有能力为用户创造引人入胜的沉浸式体验。因此,让我们踏上这一旅程,并通过视频SDK解锁互动实时流的潜力!

先决条件

首先,您的开发环境应满足以下要求:

信息
应该有一个视频SDK帐户来生成令牌。访问视频SDK dashboard生成令牌

开始使用代码!

按照以下步骤创建必要的环境,以在应用程序中添加Interactive实时流。另外,您可以找到QuickStart here的代码样本。

创建新的Android项目

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

视频SDK Android快速启动新项目

小心
创建项目后,Android Studio自动启动Gradle Sync。确保同步在继续之前成功。

集成视频SDK

Maven Central

settings.gradle

dependencyResolutionManagement {
  repositories {
    // ...
    google()
    mavenCentral()
    maven { url "https://maven.aliyun.com/repository/jcenter" }
  }
}

注意
您可以在RTC Android-SDK版本0.1.12之后使用Maven Central使用导入。无论是在Maven还是Jitpack上,相同的版本号始终参考相同的SDK。

在您的应用程序的app/build.gradle中添加以下依赖性。

app/build.gradle
dependencies {implementation 'live.videosdk:rtc-android-sdk:0.1.17'
// library to perform Network call to generate a meeting idimplementation 'com.amitshekhar.android:android-networking:1.0.2'
// other app dependencies}

信息
Android SDK与armeabi-v7aarm64-v8ax86_64体系结构兼容。如果要在模拟器中运行该应用程序,请在创建设备时选择ABI x86_64

将权限添加到您的项目中

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

AndroidManifest.xml

<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" />

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

项目结构

您的项目结构应该看起来像这样:

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.xml

注意
您必须将JoinActivity设置为启动器活动。

使用视频SDK构建Android Interactive Live流应用程序的4个步骤

步骤1:创建加入屏幕

创建一个名为joinActivity的新活动。

创建UI
加入屏幕包括:

  • 创建按钮:创建新的会议。
  • textfield的会议ID:包含您要加入的会议ID。
  • 加入作为主机按钮:加入会议作为主持人提供提供的会议。
  • 加入查看器按钮:与提供的会议ID一起以观众的身份加入会议。

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

<?xml version="1.0" encoding="utf-8"?>

<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

  • JoinActivity中创建Field sampleToken,该Field12将在Video SDK仪表板中保存生成的令牌。该令牌将用于VideosDK配置以及生成MeetingID。
public class JoinActivity extends AppCompatActivity {

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

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    //...
  }
}
  • 在JOIN按钮作为主机onClick事件上,我们将使用令牌,会议和模式作为会议导航到MeetingActivity

  • 在JOIN按钮上作为查看器onClick事件,我们将使用令牌,会议和模式作为查看器导航到MeetingActivity

public class JoinActivity extends AppCompatActivity {

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

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_join);

    final Button btnCreate = findViewById(R.id.btnCreateMeeting);
    final Button btnJoinHost = findViewById(R.id.btnJoinHostMeeting);
    final Button btnJoinViewer = findViewById(R.id.btnJoinViewerMeeting);
    final EditText etMeetingId = findViewById(R.id.etMeetingId);

    // create meeting and join as Host
    btnCreate.setOnClickListener(v -> createMeeting(sampleToken));

    // Join as Host
    btnJoinHost.setOnClickListener(v -> {
        Intent intent = new Intent(JoinActivity.this, MeetingActivity.class);
        intent.putExtra("token", sampleToken);
        intent.putExtra("meetingId", etMeetingId.getText().toString().trim());
        intent.putExtra("mode", "CONFERENCE");
        startActivity(intent);
    });

    // Join as Viewer
    btnJoinViewer.setOnClickListener(v -> {
        Intent intent = new Intent(JoinActivity.this, MeetingActivity.class);
        intent.putExtra("token", sampleToken);
        intent.putExtra("meetingId", etMeetingId.getText().toString().trim());
        intent.putExtra("mode", "VIEWER");
        startActivity(intent);
    });
  }

  private void createMeeting(String token) {
    // we will explore this method in the next step
  }
}
  • 对于createMeeting方法下的创建按钮,我们将通过拨打API并通过令牌,生成的会议ID和模式为CONFERENCE来生成METICID。
public class JoinActivity extends AppCompatActivity {
  //...onCreate

  private void createMeeting(String token) {
    // 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(new JSONObjectRequestListener() {
              @Override
              public void onResponse(JSONObject response) {
                try {
                  // response will contain `roomId`
                  final String meetingId = response.getString("roomId");

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

              @Override
              public void onError(ANError anError) {
                anError.printStackTrace();
                Toast.makeText(JoinActivity.this, anError.getMessage(), Toast.LENGTH_SHORT).show();
              }
          });
  }
}

注意
不要在房间和会议关键字之间感到困惑,这两个都是同一件事。

  • 我们的应用程序完全基于音频和视频通信,这就是为什么我们需要要求运行时权限RECORD_AUDIOCAMERA。因此,我们将在JoinActivity上实现权限逻辑。
public class JoinActivity extends AppCompatActivity {
  private static final int PERMISSION_REQ_ID = 22;

  private static final String[] REQUESTED_PERMISSIONS = {
    Manifest.permission.RECORD_AUDIO,
    Manifest.permission.CAMERA
  };

  private void checkSelfPermission(String permission, int requestCode) {
    if (ContextCompat.checkSelfPermission(this, permission) !=
            PackageManager.PERMISSION_GRANTED) {
      ActivityCompat.requestPermissions(this, REQUESTED_PERMISSIONS, requestCode);
    }
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    //... button listeneres
   checkSelfPermission(REQUESTED_PERMISSIONS[0], PERMISSION_REQ_ID);
   checkSelfPermission(REQUESTED_PERMISSIONS[1], PERMISSION_REQ_ID);
  }
}

步骤2:创建会议屏幕

创建一个名为MeetingActivity的新活动。

为会议屏幕创建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. 初始化视频SDK
  2. 配置**视频SDK **带有令牌。
  3. 用所需的参数(例如meetingIdparticipantNamemicEnabledwebcamEnabledwebcamEnabledmode等)初始化会议。
  4. 使用meeting.join()方法加入房间。
  5. 添加MeetingEventListener以聆听会议加入活动。
  6. 检查localParticipant的模式,如果该模式为会议,我们将用SpeakerFragment替换MainLayout,否则将用ViewerFragment替换。
public class MeetingActivity extends AppCompatActivity {
  private Meeting meeting;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_meeting);

    final String meetingId = getIntent().getStringExtra("meetingId");
    String token = getIntent().getStringExtra("token");
    String mode = getIntent().getStringExtra("mode");
    String localParticipantName = "John Doe";
    boolean streamEnable = mode.equals("CONFERENCE");

    // initialize VideoSDK
    VideoSDK.initialize(getApplicationContext());

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

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

    // join Meeting
    meeting.join();

    // if mode is CONFERENCE than replace mainLayout with SpeakerFragment otherwise with ViewerFragment
    meeting.addEventListener(new MeetingEventListener() {
        @Override
        public void onMeetingJoined() {
          if (meeting != null) {
            if (mode.equals("CONFERENCE")) {
              //pin the local partcipant
              meeting.getLocalParticipant().pin("SHARE_AND_CAM");
              getSupportFragmentManager()
                  .beginTransaction()
                  .replace(R.id.mainLayout, new SpeakerFragment(), "MainFragment")
                  .commit();
            } else if (mode.equals("VIEWER")) {
              getSupportFragmentManager()
                  .beginTransaction()
                  .replace(R.id.mainLayout, new ViewerFragment(), "viewerFragment")
                  .commit();
            }
          }
        }
      });
  }
  public Meeting getMeeting() {
      return meeting;
  }
}

步骤3:实施speakerviewâ

成功参加会议后,是时候渲染说话者的视图并管理控制型网络摄像头/麦克风,开始/停止HLS并离开会议了。

  • 创建一个名为SpeakerFragment的新片段。
  • /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>
  • 现在,让我们设置侦听器的按钮,该按钮将允许参与者切换媒体。
public class SpeakerFragment extends Fragment {

  private static Activity mActivity;
  private static Context mContext;
  private static Meeting meeting;
  private boolean micEnabled = true;
  private boolean webcamEnabled = true;
  private boolean hlsEnabled = false;
  private Button btnMic, btnWebcam, btnHls, btnLeave;
  private TextView tvMeetingId, tvHlsState;

  public SpeakerFragment() {
    // Required empty public constructor
  }

  @Override
  public void onAttach(@NonNull Context context) {
    super.onAttach(context);
    mContext = context;
    if (context instanceof Activity) {
      mActivity = (Activity) context;
      // getting meeting object from Meeting Activity
      meeting = ((MeetingActivity) mActivity).getMeeting();
    }
  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
                            Bundle savedInstanceState) {
    // Inflate the layout for this fragment
    View 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.setText("Meeting Id : " + meeting.getMeetingId());
        setActionListeners();
    }
    return view;
  }

  private void setActionListeners() {
    btnMic.setOnClickListener(v -> {
        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(v -> {
        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(v -> meeting.leave());

    btnHls.setOnClickListener(v -> {
      if (!hlsEnabled) {
        JSONObject config = new JSONObject();
        JSONObject layout = new 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();
      }
    });
  }
}
  • 在添加按钮的侦听器后,让我们将MeetingEventListener添加到会议中,然后在onDestroy()方法中删除所有听众。
public class SpeakerFragment extends Fragment {

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
                            Bundle savedInstanceState) {
    //...
    if (meeting != null) {
        //...
        // add Listener to the meeting
        meeting.addEventListener(meetingEventListener);
    }
    return view;
  }

  private final MeetingEventListener meetingEventListener = new MeetingEventListener() {
    @Override
    public void onMeetingLeft() {
      //unpin local participant
      meeting.getLocalParticipant().unpin("SHARE_AND_CAM");
      if (isAdded()) {
        Intent intents = new Intent(mContext, JoinActivity.class);
        intents.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
                | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        startActivity(intents);
        mActivity.finish();
      }
    }

    @RequiresApi(api = Build.VERSION_CODES.P)
    @Override
    public void onHlsStateChanged(JSONObject HlsState) {
        if (HlsState.has("status")) {
          try {
            tvHlsState.setText("Current HLS State : " + HlsState.getString("status"));
            if (HlsState.getString("status").equals("HLS_STARTED")) {
                hlsEnabled=true;
                btnHls.setText("Stop HLS");
            }
            if (HlsState.getString("status").equals("HLS_STOPPED")) {
                hlsEnabled = false;
                btnHls.setText("Start HLS");
            }
          } catch (JSONException e) {
              e.printStackTrace();
          }
        }
    }
  };

  @Override
  public void onDestroy() {
    mContext = null;
    mActivity = null;
    if (meeting != null) {
        meeting.removeAllListeners();
        meeting = null;
    }
    super.onDestroy();
  }

}
  • 下一步是呈现说话者的观点。使用RecyClerview,我们将展示加入会议的主持人的参与者列表。

信息

  • 在这里,参与者的视频是使用视频视频显示的,但是您也可以使用SurfaceViewRender进行。
  • 对于视频,SDK版本应为0.1.13或更高。
  • 要了解有关视频的更多信息,请访问here
  • 为参与者视图创建一个新的布局,名为Item_remote_peer.xml在Res/Layout文件夹中。
<?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。
public class SpeakerAdapter extends RecyclerView.Adapter<SpeakerAdapter.PeerViewHolder> {
  private List<Participant> participantList = new ArrayList<>();
  private final Meeting meeting;
  public SpeakerAdapter(Meeting meeting) {
    this.meeting = meeting;

    updateParticipantList();

    // adding Meeting Event listener to get the participant join/leave event in the meeting.
    meeting.addEventListener(new MeetingEventListener() {
      @Override
      public void onParticipantJoined(Participant participant) {
        // check participant join as Host/Speaker or not
        if (participant.getMode().equals("CONFERENCE")) {
          // pin the participant
          participant.pin("SHARE_AND_CAM");
          // add participant in participantList
          participantList.add(participant);
        }
        notifyDataSetChanged();
      }

      @Override
      public void onParticipantLeft(Participant participant) {
        int pos = -1;
        for (int i = 0; i < participantList.size(); i++) {
          if (participantList.get(i).getId().equals(participant.getId())) {
            pos = i;
            break;
          }
        }
        if(participantList.contains(participant)) {
          // unpin participant who left the meeting
          participant.unpin("SHARE_AND_CAM");
          // remove participant from the list
          participantList.remove(participant);
        }
        if (pos >= 0) {
            notifyItemRemoved(pos);
        }
      }
    });
  }

  private void updateParticipantList() {
    participantList = new ArrayList<>();

    // adding the local participant(You) to the list
    participantList.add(meeting.getLocalParticipant());

    // adding participants who join as Host/Speaker
    Iterator<Participant> participants = meeting.getParticipants().values().iterator();
    for (int i = 0; i < meeting.getParticipants().size(); i++) {
      final Participant participant = participants.next();
      if (participant.getMode().equals("CONFERENCE")) {
        // pin the participant
        participant.pin("SHARE_AND_CAM");
        // add participant in participantList
        participantList.add(participant);
      }
    }
  }

  @NonNull
  @Override
  public PeerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    return new PeerViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_remote_peer, parent, false));
  }

  @Override
  public void onBindViewHolder(@NonNull PeerViewHolder holder, int position) {
    Participant participant = participantList.get(position);

    holder.tvName.setText(participant.getDisplayName());

    // adding the initial video stream for the participant into the 'VideoView'
    for (Map.Entry<String, Stream> entry : participant.getStreams().entrySet()) {
      Stream stream = entry.getValue();
      if (stream.getKind().equalsIgnoreCase("video")) {
        holder.participantView.setVisibility(View.VISIBLE);
        VideoTrack videoTrack = (VideoTrack) stream.getTrack();
        holder.participantView.addTrack(videoTrack);
        break;
      }
    }

    // add Listener to the participant which will update start or stop the video stream of that participant
    participant.addEventListener(new ParticipantEventListener() {
        @Override
        public void onStreamEnabled(Stream stream) {
          if (stream.getKind().equalsIgnoreCase("video")) {
            holder.participantView.setVisibility(View.VISIBLE);
            VideoTrack videoTrack = (VideoTrack) stream.getTrack();
            holder.participantView.addTrack(videoTrack);
          }
        }

        @Override
        public void onStreamDisabled(Stream stream) {
          if (stream.getKind().equalsIgnoreCase("video")) {
            holder.participantView.removeTrack();
            holder.participantView.setVisibility(View.GONE);
          }
        }
    });
  }

  @Override
  public int getItemCount() {
    return participantList.size();
  }

  static class PeerViewHolder extends RecyclerView.ViewHolder {
    // 'VideoView' to show Video Stream
    public VideoView participantView;
    public TextView tvName;
    public View itemView;

    PeerViewHolder(@NonNull View view) {
        super(view);
        itemView = view;
        tvName = view.findViewById(R.id.tvName);
        participantView = view.findViewById(R.id.participantView);
    }
  }
}
  • 将此适配器添加到SpeakerFragment
 @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
                            Bundle savedInstanceState) {
    //...
    if (meeting != null) {
      //...
      final RecyclerView rvParticipants = view.findViewById(R.id.rvParticipants);
      rvParticipants.setLayoutManager(new GridLayoutManager(mContext, 2));
      rvParticipants.setAdapter(new SpeakerAdapter(meeting));
    }
}

步骤4:实现Viewerview

当主机开始实时流媒体时,观看者将能够看到实时流。

要实现播放器视图,我们将使用ExoPlayer。玩HLS流将很有帮助。

,但首先,让我们在项目中添加依赖。

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

创建一个名为ViewerFragment的新片段

创建UIâ

观众片段将包括:

  • 会议ID的文本视图 - 您加入的会议ID将显示在此文本视图中。
  • 离开按钮 - 此按钮将离开会议。
  • 等待layout - 这是当没有活动HLS时显示的TextView。
  • 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流

初始化播放器并在HLS状态为HLS_PLAYABLE时播放HLS,并在HLS状态为HLS_STOPPED时将其发布。每当HLS状态更改会议时,将触发事件onHlsStateChanged

public class ViewerFragment extends Fragment {

  private Meeting meeting;
  protected StyledPlayerView playerView;
  private TextView waitingLayout;
  protected @Nullable
  ExoPlayer player;

  private DefaultHttpDataSource.Factory dataSourceFactory;
  private boolean startAutoPlay=true;
  private String downStreamUrl = "";
  private static Activity mActivity;
  private static Context mContext;

  public ViewerFragment() {
      // Required empty public constructor
  }

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
  }

  @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container,
                            Bundle savedInstanceState) {
    // Inflate the layout for this fragment
    View 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
      ((TextView) view.findViewById(R.id.meetingId)).setText("Meeting Id : " + meeting.getMeetingId());
      // leave the meeting on btnLeave click
      ((Button) view.findViewById(R.id.btnLeave)).setOnClickListener(v -> meeting.leave());
      // add listener to meeting
      meeting.addEventListener(meetingEventListener);
    }
    return view;
  }


  @Override
  public void onAttach(@NonNull Context context) {
    super.onAttach(context);
    mContext = context;
    if (context instanceof Activity) {
      mActivity = (Activity) context;
      // get meeting object from MeetingActivity
      meeting = ((MeetingActivity) mActivity).getMeeting();
    }
  }

  private final MeetingEventListener meetingEventListener = new MeetingEventListener() {

      @Override
      public void onMeetingLeft() {
        if (isAdded()) {
          Intent intents = new Intent(mContext, JoinActivity.class);
          intents.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
                  | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_CLEAR_TASK);
          startActivity(intents);
          mActivity.finish();
        }
      }

      @RequiresApi(api = Build.VERSION_CODES.P)
      @Override
      public void onHlsStateChanged(JSONObject HlsState) {
        if (HlsState.has("status")) {
          try {
            if (HlsState.getString("status").equals("HLS_PLAYABLE") && HlsState.has("downstreamUrl")) {
              downStreamUrl = HlsState.getString("downstreamUrl");
              waitingLayout.setVisibility(View.GONE);
              playerView.setVisibility(View.VISIBLE);
              // initialize player
              initializePlayer();
            }
            if (HlsState.getString("status").equals("HLS_STOPPED")) {
              // release the player
              releasePlayer();
              downStreamUrl = null;
              waitingLayout.setText("Host has stopped \n the live streaming");
              waitingLayout.setVisibility(View.VISIBLE);
              playerView.setVisibility(View.GONE);
            }
          } catch (JSONException e) {
              e.printStackTrace();
          }
        }
    }
  };

  protected void initializePlayer() {
    if (player == null) {
      dataSourceFactory = new DefaultHttpDataSource.Factory();
      HlsMediaSource mediaSource = new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(
              MediaItem.fromUri(Uri.parse(this.downStreamUrl)));
      ExoPlayer.Builder playerBuilder =
              new ExoPlayer.Builder(/* context= */ mContext);
      player = playerBuilder.build();
      // auto play when player is ready
      player.setPlayWhenReady(startAutoPlay);
      player.setMediaSource(mediaSource);
      // if you want display setting for player then remove this line
      playerView.findViewById(com.google.android.exoplayer2.ui.R.id.exo_settings).setVisibility(View.GONE);
      playerView.setPlayer(player);
    }
    player.prepare();
  }

  protected void releasePlayer() {
    if (player != null) {
      player.release();
      player = null;
      dataSourceFactory = null;
      playerView.setPlayer(/* player= */ null);
    }
  }

  @Override
  public void onDestroy() {
    mContext = null;
    mActivity = null;
    downStreamUrl = null;
    releasePlayer();
    if (meeting != null) {
      meeting.removeAllListeners();
      meeting = null;
    }
    super.onDestroy();
  }

}

最终输出

使用Video SDK和Java在Android中实现自定义的视频通话应用程序。要探索更多功能,请浏览基本和高级功能。

结论

恭喜!通过遵循本教程,您使用Video SDK成功地将Interactive Live流功能集成到了Android应用中。让我们回顾一下您已经完成的工作:

  • 创建了一个用户友好的加入屏幕,该屏幕允许用户输入会议ID并选择加入作为主机或观看者。
  • 实现了生成会议ID并处理不同角色(主机/查看器)的必要逻辑。
  • 开发了一个会议屏幕,用户可以参加互动式实时流媒体会议,与流互动,与其他参与者聊天并进行实时讨论。

更多的Android资源