Android Media Devices¶
Пример Android-приложения для управления медиа-устройствами¶
Данный пример может использоваться как стример для публикации WebRTC-видеопотока с Web Call Server и позволяет выбрать медиа-устройства и параметры для публикуемого видео
На скриншоте ниже отображаюсчя
- слева - видео с выбранной камеры (основная тыловая камера устройства)
- справа - опубликованный поток с сервера
Также показаны основные поля ввода параметров видео

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

Работа с кодом примера¶
Для разбора кода возьмем класс MediaDevicesActivity.java примера media-devices, который доступен для скачивания в соответствующей сборке 1.1.0.69.
1. Инициализация API¶
Flashphoner.init() code
При инициализации методу init() передается объект Сontext.
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
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
13. Воспроизведение потока с сервера¶
Stream.play() code
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
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();
}
});
}