Перейти к содержанию

Android Media Devices

Пример Android-приложения для управления медиа-устройствами

Данный пример может использоваться как стример для публикации WebRTC-видеопотока с Web Call Server и позволяет выбрать медиа-устройства и параметры для публикуемого видео

На скриншоте ниже отображаюсчя

  • слева - видео с выбранной камеры (основная тыловая камера устройства)
  • справа - опубликованный поток с сервера

Также показаны основные поля ввода параметров видео

Переключение камеры и объекта для вывода изображения с камеры

Работа с кодом примера

Для разбора кода возьмем класс MediaDevicesActivity.java примера media-devices, который доступен для скачивания в соответствующей сборке 1.1.0.69.

1. Инициализация API

Flashphoner.init() code

При инициализации методу init() передается объект Сontext.

Flashphoner.init(this);

2. Получение списка доступных медиа-устройств

Flashphoner.getMediaDevices(), MediaDeviceList.getAudioList(), MediaDeviceList.getVideoList() code

mMicSpinner = (Spinner) findViewById(R.id.microphone);
ArrayAdapter<MediaDevice> micAdapter = new ArrayAdapter<>(
        this,
        android.R.layout.simple_spinner_item,
        Flashphoner.getMediaDevices().getAudioList()
);
micAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mMicSpinner.setAdapter(micAdapter);
...
mCameraSpinner = (Spinner) findViewById(R.id.camera);
ArrayAdapter<MediaDevice> camAdapter = new ArrayAdapter<>(
        this,
        android.R.layout.simple_spinner_item,
        Flashphoner.getMediaDevices().getVideoList()
);
camAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
mCameraSpinner.setAdapter(camAdapter);

3. Управление отображением видео

FPSurfaceViewRenderer.setMirror() code

При показе видео изображение выводится на объекты FPSurfaceViewRenderer:

  • localRender для отображения видео с камеры
  • remoteRender для отображения публикуемого потока
  • newSurfaceRenderer для демонстрации переключения объекта

Для этих объектов устанавливается позиция на экране, тип масштабирования и зеркалирование.

По умолчанию, для отображения видео с камеры устанавливается зеркальная ориентация при помощи метода setMirror(true). Для отображения публикуемого потока и объекта для демонстрации переключения зеркалирование отключается при помощи setMirror(false):

remoteRenderLayout.setPosition(0, 0, 100, 100);
remoteRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
remoteRender.setMirror(false);
remoteRender.requestLayout();

localRenderLayout.setPosition(0, 0, 100, 100);
localRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
localRender.setMirror(true);
localRender.requestLayout();

switchRenderLayout.setPosition(0, 0, 100, 100);
newSurfaceRenderer.setZOrderMediaOverlay(true);
newSurfaceRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
newSurfaceRenderer.setMirror(true);
newSurfaceRenderer.requestLayout();

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

4. Настройка параметров аудио и видео, заданных пользователем

AudioConstraints, VideoConstraints code

@NonNull
private Constraints getConstraints() {
    AudioConstraints audioConstraints = null;
    if (mSendAudio.isChecked()) {
        audioConstraints = new AudioConstraints();
        if (mUseFEC.isChecked()) {
            audioConstraints.setUseFEC(true);
        }
        if (mUseStereo.isChecked()) {
            audioConstraints.setUseStereo(true);
        }
        if (!mDefaultPublishAudioBitrate.isChecked() && mDefaultPublishAudioBitrate.getText().length() > 0) {
            audioConstraints.setBitrate(Integer.parseInt(mPublishAudioBitrate.getText().toString()));
        }
    }
    VideoConstraints videoConstraints = null;
    if (mSendVideo.isChecked()) {
        videoConstraints = new VideoConstraints();
        videoConstraints.setCameraId(((MediaDevice) mCameraSpinner.getSpinner().getSelectedItem()).getId());
        if (mCameraFPS.getText().length() > 0) {
            videoConstraints.setVideoFps(Integer.parseInt(mCameraFPS.getText().toString()));
        }
        if (mWidth.getText().length() > 0 && mHeight.getText().length() > 0) {
            videoConstraints.setResolution(Integer.parseInt(mWidth.getText().toString()),
                    Integer.parseInt(mHeight.getText().toString()));
        }
        if (!mDefaultPublishVideoBitrate.isChecked() && mPublishVideoBitrate.getText().length() > 0) {
             videoConstraints.setBitrate(Integer.parseInt(mPublishVideoBitrate.getText().toString()));
        }
    }
    return new Constraints(audioConstraints, videoConstraints);
}

5. Локальное тестирование камеры и микрофона

Flashphoner.getLocalMediaAccess() code

Методу передаются:

  • настройки аудио и видео, заданные пользователем
  • локальный объект SurfaceViewRenderer localRenderer для вывода изображения с выбранной камеры
case TEST_REQUEST_CODE: {
    if (grantResults.length == 0 ||
          grantResults[0] != PackageManager.PERMISSION_GRANTED ||
          grantResults[1] != PackageManager.PERMISSION_GRANTED) {
        Log.i(TAG, "Permission has been denied by user");
    } else {
        Flashphoner.getLocalMediaAccess(getConstraints(), localRender);
        mTestButton.setText(R.string.action_release);
        mTestButton.setTag(R.string.action_release);
        mStartButton.setEnabled(false);
        soundMeter = new SoundMeter();
        soundMeter.start();
        ...
        Log.i(TAG, "Permission has been granted by user");
    }
break;

6. Создание сессии

Flashphoner.createSession() code

Методу передается объект SessionOptions со следующими параметрами

  • URL WCS-сервера
  • SurfaceViewRenderer localRenderer, который будет использоваться для отображения видео с камеры
  • SurfaceViewRenderer remoteRenderer, который будет использоваться для воспроизведения опубликованного видеопотока
SessionOptions sessionOptions = new SessionOptions(url);

/**
  * Session for connection to WCS server is created with method createSession().
  */
session = Flashphoner.createSession(sessionOptions);

7. Подключение к серверу

Session.connect() code

session.connect(new Connection(), getBasicAuthHeader(url));

8. Получение от сервера события, подтверждающего успешное соединение

Session.onConnected() code

@Override
public void onConnected(final Connection connection) {
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            mStatusView.setText(connection.getStatus());
            mPublishButton.setEnabled(true);
            mPlayButton.setEnabled(true);
            //onStarted();
            MediaDevicesActivity.this.onConnected();
        }
    });
}

9. Создание потока для публикации

Session.createStream() code

publishStream = session.createStream(streamOptions);
if (mMuteAudio.isChecked()) {
    publishStream.muteAudio();
}
if (mMuteVideo.isChecked()) {
    publishStream.muteVideo();
}
...

ActivityCompat.requestPermissions(MediaDevicesActivity.this,
         new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA},
         PUBLISH_REQUEST_CODE);

10. Публикация потока

Stream.publish() code

case PUBLISH_REQUEST_CODE: {
    if (grantResults.length == 0 ||
           grantResults[0] != PackageManager.PERMISSION_GRANTED ||
           grantResults[1] != PackageManager.PERMISSION_GRANTED) {
        mStartButton.setEnabled(false);
        mTestButton.setEnabled(false);
        session.disconnect();
        Log.i(TAG, "Permission has been denied by user");
    } else {
        /**
          * Method Stream.publish() is called to publish stream.
          */
        publishStream.publish();
        Log.i(TAG, "Permission has been granted by user");
    }
    break;
}

11. Получение от сервера события, подтверждающего успешную публикацию потока

StreamStatusEvent.PUBLISHING code

publishStream.on(new StreamEventHandler() {
    @Override
    public void onStreamStatus(final Stream stream, final StreamStatus streamStatus) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (StreamStatus.PUBLISHING.equals(streamStatus)) {
                    onPublished();
                } else {
                    onUnpublished();
                    Log.e(TAG, "Can not publish stream " + stream.getName() + " " + streamStatus);
                }
                mStatusView.setText(streamStatus.toString());
            }
        });
    }

    @Override
    public void onStreamEvent(StreamEvent streamEvent) {

    }
});

12. Создание потока для воспроизведения с сервера

Session.createStream() code

playStream = session.createStream(streamOptions);

13. Воспроизведение потока с сервера

Stream.play() code

playStream.play();

14. Получение от сервера события, подтверждающего успешное воспроизведение потока

StreamStatusEvent.PLAYING code

playStream.on(new StreamEventHandler() {
    @Override
    public void onStreamStatus(final Stream stream, final StreamStatus streamStatus) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (!StreamStatus.PLAYING.equals(streamStatus)) {
                    onStoppedPlay();
                    Log.e(TAG, "Can not play stream " + stream.getName() + " " + streamStatus);
                } else {
                    onPlayed(stream);
                    Flashphoner.setVolume(mPlayVolume.getProgress());
                }
                mStatusView.setText(streamStatus.toString());
            }
        });
    }

    @Override
    public void onStreamEvent(StreamEvent streamEvent) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (streamEvent.getPayload() != null) {
                    mMutedName.setText(getString(R.string.muted_name) + streamEvent.getPayload().get("streamName"));
                }
                switch (streamEvent.getType()) {
                    case audioMuted:
                        mAudioMuteStatus.setText(getString(R.string.audio_mute_status) + "true");
                        break;
                    case audioUnmuted:
                        mAudioMuteStatus.setText(getString(R.string.audio_mute_status) + "false");
                        break;
                    case videoMuted:
                        mVideoMuteStatus.setText(getString(R.string.video_mute_status) + "true");
                        break;
                    case videoUnmuted:
                        mVideoMuteStatus.setText(getString(R.string.video_mute_status) + "false");
                }
            }
        });
    }
});

15. Переключение на следующую камеру во время трансляции

Stream.switchCamera() code

private void switchToCamera() {
    if (publishStream != null) {
        turnOffFlashlight();
        muteButton();
        publishStream.switchCamera(new CameraSwitchHandler() {
            @Override
            public void onCameraSwitchDone(boolean var1) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if (mConnectButton.getTag() == null || Integer.valueOf(R.string.action_disconnect).equals(mConnectButton.getTag())) {
                            onConnected();
                            onPublished();
                        }
                    }
                });

            }

            @Override
            public void onCameraSwitchError(String var1) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        onConnected();
                        onPublished();
                    }
                });
            }
        });
    }
}

16. Переключение на определенную камеру по имени во время трансляции

Stream.switchCamera() code

private void switchToCamera(String name) {
    if (publishStream != null) {
        turnOffFlashlight();
        muteButton();
        publishStream.switchCamera(new CameraSwitchHandler() {
            @Override
            public void onCameraSwitchDone(boolean var1) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if (mConnectButton.getTag() == null || Integer.valueOf(R.string.action_disconnect).equals(mConnectButton.getTag())) {
                            onConnected();
                            onPublished();
                        }
                    }
                });
            }

            @Override
            public void onCameraSwitchError(String var1) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        onConnected();
                        onPublished();
                    }
                });
            }
        }, name);
    }
}

17. Переключение объекта для отображения видеопотока во время трансляции

Stream.switchRenderer() code

private void switchRender() {
    if (spinner.getSelectedItemId() == 0) {
        if (isSwitchLocalRenderer) {
            publishStream.switchRenderer(localRender);
            isSwitchLocalRenderer = false;
        } else {
            publishStream.switchRenderer(newSurfaceRenderer);
            isSwitchLocalRenderer = true;
        }
    } else {
        if (isSwitchRemoteRenderer) {
            playStream.switchRenderer(remoteRender);
            isSwitchRemoteRenderer = false;
        } else {
            playStream.switchRenderer(newSurfaceRenderer);
            isSwitchRemoteRenderer = true;
        }
    }
}

18. Управление звуком при помощи аппаратных кнопок

Flashphoner.setVolume() code

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        int currentVolume = Flashphoner.getVolume();
        switch (keyCode) {
            case KeyEvent.KEYCODE_VOLUME_DOWN:
                if (currentVolume == 1) {
                    Flashphoner.setVolume(0);
                }
                mPlayVolume.setProgress(currentVolume-1);
                break;
            case KeyEvent.KEYCODE_VOLUME_UP:
                if (currentVolume == 0) {
                    Flashphoner.setVolume(1);
                }
                mPlayVolume.setProgress(currentVolume+1);
                break;
        }
        return super.onKeyDown(keyCode, event);
    }

19. Использование внешнего динамика телефона

Flashphoner.getAudioManager().setUseSpeakerPhone() code

mAudioOutput.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
    @Override
    public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
        String audioType = (String) adapterView.getItemAtPosition(i);
        switch (audioType) {
            case "speakerphone":
                Flashphoner.getAudioManager().setUseSpeakerPhone(true);
                break;
            case "phone":
                Flashphoner.getAudioManager().setUseBluetoothSco(false);
                Flashphoner.getAudioManager().setUseSpeakerPhone(false);
                break;
            case "bluetooth":
                Flashphoner.getAudioManager().setUseBluetoothSco(true);
                break;
        }
    }

    @Override
    public void onNothingSelected(AdapterView<?> adapterView) {

    }
});

20. Закрытие соединения

Session.disconnect() code

session.disconnect();

21. Получение события, подтверждающего разъединение

Session.onDisconnection() code

@Override
public void onDisconnection(final Connection connection) {
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            mStatusView.setText(connection.getStatus());
            ...
            mPublishButton.setEnabled(false);
            mPlayButton.setEnabled(false);
            //onStopped();
            MediaDevicesActivity.this.onDisconnected();
        }
    });
}