Объяснение того, что происходит в кратком руководстве

Что делает Твилио?

Он выполняет всю тяжелую работу по управлению пользователями в комнате и пересылке видео- и аудиодорожек между ними. По сути, все, что нам осталось сделать, это…

  1. Настройте пользовательский интерфейс и среду для подключения
  2. Обрабатывать пользовательские события

Как использовать программируемое видео Twilio

Все в этой статье взято из документации Twilio и ее проекта быстрого старта.

Сначала мы добавляем предоставленный SDK Twilio. Он включает в себя все необходимые функции и объекты.

// inside our app's build.gradle
// Twilio video sdk
implementation 'com.twilio:video-android:$version'

Настройте отображаемый вид

Затем мы можем использовать videoView из SDK для отображения видео на экране.

import com.twilio.video.VideoView;

Версия xml

<com.twilio.video.VideoView
    android:id="@+id/primary_video_view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:keepScreenOn="true" />

и версия кода

VideoView v = new VideoView(context);
v.setKeepScreenOn(true);

Держите устройство в бодрствующем состоянии

Обратите внимание, я установил атрибут setKeepScreenon, потому что мы, скорее всего, не хотим, чтобы телефон переходил в спящий режим во время видеозвонка. Очень удобно, что keepScreenOn можно использовать с любым дочерним представлением и оно будет работать так же, как и в родительском представлении.

Подключение к комнате

Создать токен доступа

Каждый пользователь должен использовать токен доступа для подключения к комнате. Он уникален для каждого пользователя в каждой комнате для совещаний. Этот шаг обычно выполняется в бэкенде.

// taken from https://www.twilio.com/docs/iam/access-tokens?code-sample=code-creating-an-access-token-video-2&code-language=Java&code-sdk-version=7.x
import com.twilio.jwt.accesstoken.AccessToken;
import com.twilio.jwt.accesstoken.VideoGrant;

public class TokenGenerator {

  public static void main(String[] args) {
    // Required for all types of tokens
    String twilioAccountSid = "ACxxxxxxxxxxxx";
    String twilioApiKey = "SKxxxxxxxxxxxx";
    String twilioApiSecret = "xxxxxxxxxxxxxx";

    // Required for Video
    String identity = "user";

    // Create Video grant
    VideoGrant grant = new VideoGrant().setRoom("cool room");

    // Create access token
    AccessToken token = new AccessToken.Builder(
      twilioAccountSid,
      twilioApiKey,
      twilioApiSecret
    ).identity(identity).grant(grant).build();

    System.out.println(token.toJwt());
  }
}

Затем в нашем внешнем приложении мы можем подключиться к комнате, используя токен доступа. roomName уже определено при генерации токена доступа, поэтому аргумент здесь не повлияет на то, к какой комнате мы на самом деле будем подключаться.

// taken from https://www.twilio.com/docs/video/android-getting-started
public void connectToRoom(String roomName) {
  ConnectOptions connectOptions = new ConnectOptions.Builder(accessToken)
    .roomName(roomName)
    .audioTracks(localAudioTracks)
    .videoTracks(localVideoTracks)
    .dataTracks(localDataTracks)
    .build();
  room = Video.connect(context, connectOptions, roomListener);
}

Создать локальную видео- и аудиодорожку

Чтобы connectToRoom() заработало, нам нужно поделиться своим видео и голосом с другими участниками через треки.

private void createAudioAndVideoTracks() {
    // Share your microphone
    localAudioTrack = LocalAudioTrack.create(this, true, LOCAL_AUDIO_TRACK_NAME);

    // Share your camera
    cameraCapturerCompat = new CameraCapturerCompat(this, getAvailableCameraSource());
    localVideoTrack = LocalVideoTrack.create(this,
            true,
            cameraCapturerCompat.getVideoCapturer(),
            LOCAL_VIDEO_TRACK_NAME);
    primaryVideoView.setMirror(true);
    localVideoTrack.addRenderer(primaryVideoView);
    localVideoView = primaryVideoView;
}

Запросить разрешение на микрофон и камеру

В нашем манифесте нам понадобится все это.

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

Тогда, наконец, мы можем проверить и запросить разрешение.

private boolean checkPermissionForCameraAndMicrophone() {
        int resultCamera = ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA);
        int resultMic = ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO);
        return resultCamera == PackageManager.PERMISSION_GRANTED &&
                resultMic == PackageManager.PERMISSION_GRANTED;
    }

    private void requestPermissionForCameraAndMicrophone() {

            // request permission in fragment
            requestPermissions(
                    new String[]{Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO},
                    CAMERA_MIC_PERMISSION_REQUEST_CODE);

    }

И нам нужно поймать событие, когда разрешение предоставлено

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {

    if (requestCode == CAMERA_MIC_PERMISSION_REQUEST_CODE && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED
    ) {
        Toast.makeText(context, "Permission granted", Toast.LENGTH_LONG).show();
    }
}

Обработка пользовательских событий

Twilio будет сообщать о таких событиях, как подключение участника, отключение с помощью обратного вызова. Мы должны предоставить интерфейс roomListener() при подключении к комнате.

Помимо установки слушателя для комнаты, мы также должны прикрепить слушателя к каждому участнику в комнате. Этот слушатель будет отслеживать статус видео и аудио этого конкретного участника.

Мы можем увидеть полные методы для этих обратных вызовов, ознакомившись с примером быстрого запуска Twilio.

Ниже приведен пример для roomListener(), здесь мы хотим сделать так, что как только будет вызван onConnected(), мы получим всех участников в комнате и прикрепим remoteParticipantListener() к каждому из них.

Конечно, нам также нужно прикрепить слушателя к любым участникам, которые присоединятся к комнате в будущем.

private Room.Listener roomListener() {
    return new Room.Listener() {
        @Override
        public void onConnected(Room room) {
            Toast.makeText(MainActivity.this, "Connected to room " + room.getName(), Toast.LENGTH_LONG).show();

            localParticipant = room.getLocalParticipant();

            List<RemoteParticipant> remoteParticipants = room.getRemoteParticipants();

            for (RemoteParticipant remoteParticipant : remoteParticipants) {

                addRemoteParticipant(remoteParticipant);
            }

            // adding the participant does not mean their video track is ready to be subscribed
            // display the videos in the onVideoTrackSubscribed callback
        }

        @Override
        public void onParticipantConnected(Room room, RemoteParticipant remoteParticipant) {
            addRemoteParticipant(remoteParticipant);
        }

        @Override
        public void onParticipantDisconnected(Room room, RemoteParticipant remoteParticipant) {
            removeRemoteParticipant(remoteParticipant);
        }
}

Поскольку это интерфейс Java, мы должны выполнить все его необходимые методы. Но, к счастью, название метода говорит само за себя, а логика отображения и удаления видео участников из пользовательского интерфейса проста.

Ниже приведен пример обратного вызова remoteParticipantListener(). Помните, что это прослушиватель для каждого участника, поэтому обновите правильное отображение для правильного участника.

// add listener to each participant when you joined a room

@SuppressLint("SetTextI18n")
private RemoteParticipant.Listener remoteParticipantListener() {
    return new RemoteParticipant.Listener() {
        @Override
        public void onAudioTrackPublished(RemoteParticipant remoteParticipant,
                                          RemoteAudioTrackPublication remoteAudioTrackPublication) {
            Log.i(TAG, String.format("onAudioTrackPublished: " +
                            "[RemoteParticipant: identity=%s], " +
                            "[RemoteAudioTrackPublication: sid=%s, enabled=%b, " +
                            "subscribed=%b, name=%s]",
                    remoteParticipant.getIdentity(),
                    remoteAudioTrackPublication.getTrackSid(),
                    remoteAudioTrackPublication.isTrackEnabled(),
                    remoteAudioTrackPublication.isTrackSubscribed(),
                    remoteAudioTrackPublication.getTrackName()));
        }

        @Override
        public void onAudioTrackUnpublished(RemoteParticipant remoteParticipant,
                                            RemoteAudioTrackPublication remoteAudioTrackPublication) {
            Log.i(TAG, String.format("onAudioTrackUnpublished: " +
                            "[RemoteParticipant: identity=%s], " +
                            "[RemoteAudioTrackPublication: sid=%s, enabled=%b, " +
                            "subscribed=%b, name=%s]",
                    remoteParticipant.getIdentity(),
                    remoteAudioTrackPublication.getTrackSid(),
                    remoteAudioTrackPublication.isTrackEnabled(),
                    remoteAudioTrackPublication.isTrackSubscribed(),
                    remoteAudioTrackPublication.getTrackName()));
        }
......

Очистить и освободить ресурс

Наконец, не забудьте соответствующим образом обработать повторное подключение и отключение в жизненном цикле нашей активности.

Если какие-то другие приложения с более высоким приоритетом прерывают наше приложение или пользователь вручную выключает экран, нажав кнопку питания, мы должны отключиться от комнаты. Потому что пользователь может не вернуться, и Twilio продолжит считать минуты обслуживания и взимать деньги.

Поэтому наш метод onPause() должен иметь минимум отключения от комнаты и высвобождения ресурсов камеры и микрофона нашего телефона.

@Override
public void onPause(){
    super.onPause();


    /*
     * Release the local video track before going in the background. This ensures that the
     * camera can be used by other applications while this app is in the background.
     */
    if (localVideoTrack != null) {
        /*
         * If this local video track is being shared in a Room, unpublish from room before
         * releasing the video track. Participants will be notified that the track has been
         * unpublished.
         */
        if (localParticipant != null) {
            localParticipant.unpublishTrack(localVideoTrack);
        }

        localVideoTrack.release();
        localVideoTrack = null;
    }

    /*
     * Always disconnect from the room before leaving the Activity to
     * ensure any memory allocated to the Room resource is freed.
     */
    if (room != null && room.getState() != Room.State.DISCONNECTED) {
        room.disconnect();
        clearVideoDisplays();
    }
|

И в нашем onResume()мы переподключим пользователя к комнате, если пользователь вернется к нашей активности.

Как выводить звуки на разные устройства

В случае, если пользователь подключил ушной жучок или подключился к наушникам Bluetooth, наше приложение должно обнаружить это событие и направить звуковую дорожку на это устройство.

Библиотека AudioSwitch от Twilio позволяет это сделать.

Библиотека автоматически выберет устройство на основе следующего приоритета: BluetoothHeadset -> WiredHeadset -> Earpiece -> Speakerphone.

Если мы делаем приложение для видеоконференций, мы бы предпочли громкую связь, а не наушник. Мы можем установить порядок приоритетов в конструкторе.

List<Class<? extends AudioDevice>> preferredDevices = new ArrayList<>();
preferredDevices.add(BluetoothHeadset.class);
preferredDevices.add(WiredHeadset.class);
preferredDevices.add(Speakerphone.class);
AudioSwitch audioSwitch = new AudioSwitch(context, false, focusChange -> {}, preferredDevices);
  1. создать объект audioSwitch со списком приоритетов
  2. зарегистрировать обратный вызов audioSwitch.start() перед вызовом активации
  3. вызовите audioSwitch.activate(), когда connectToRoom
  4. вызывать audioSwitch.deactive() при отключении от комнаты
  5. вызовите audioSwitch.stop() в onDestroy()

Как сделать экран черным, когда другие участники отключают свое видео

Вы могли заметить, что на вашем экране будет отображаться замороженный последний кадр другого участника, когда он отключит свое видео. Более идеальный способ справиться с этим — показать черный экран.

Я не нашел никаких настроек, чтобы экран автоматически превращался в черный. Итак, в конце концов я создал макет ограничения с черным фоном. Первоначально пометьте его видимость как GONE, и когда я получаю от участника отключить обратный вызов видео, я снова устанавливаю вид на Visible, чтобы скрыть замороженный последний кадр.

Как определить основного говорящего

Иногда мы можем захотеть ответить спикеру. Например, поместить видео текущего выступающего участника в главное окно.

Twilio предоставил нам настройку enableDominantSpeaker для обнаружения этого события.

См. https://www.twilio.com/docs/video/detecting-dominant-speaker, чтобы включить этот параметр в connectOptions.

Теперь у нас есть обратный вызов onDominantSpeakerChanged.

Как не услышать эхо

Я столкнулся с проблемой звукового эха между Samsung и iPhone. Пользователь услышит свой голос с устройства.

Применение рекомендуемых настроек с https://github.com/twilio/video-quickstart-android#troubleshooting-audio устранило эту проблему.