Пример Android-приложения для управления медиа-устройствами
Данный пример может использоваться как стример для публикации WebRTC-видеопотока с Web Call Server и позволяет выбрать медиа-устройства и параметры для публикуемого видео
Пример переключения объекта для вывода изображения с камеры
Работа с кодом примера
Для разбора кода возьмем класс MediaDevicesActivity.java примера media-devices, который доступен для скачивания в соответствующей сборке 1.0.1.4670.
1. Инициализация API.
При инициализации методу init() передается объект Сontext.
Code Block | ||||
---|---|---|---|---|
| ||||
Flashphoner.init(this); |
...
Flashphoner.getMediaDevices(), MediaDeviceList.getAudioList(), MediaDeviceList.getVideoList() код code
Code Block | ||||
---|---|---|---|---|
| ||||
mMicSpinner = (LabelledSpinner) findViewById(R.id.microphone); mMicSpinner.setItemsArray(Flashphoner.getMediaDevices().getAudioList()); mMicLevel = (TextView) findViewById(R.id.microphone_level); mCameraSpinner = (LabelledSpinner) findViewById(R.id.camera); mCameraSpinner.setItemsArray(Flashphoner.getMediaDevices().getVideoList()); |
3. Настройка параметров аудио и видео, заданных пользователем
AudioConstraints, VideoConstraints код
...
language | js |
---|---|
theme | RDark |
...
Управление отображением видео
FPSurfaceViewRenderer.setMirror() code
При показе видео изображение выводится на объекты FPSurfaceViewRenderer:
- localRender для отображения видео с камеры
- remoteRender для отображения публикуемого потока
- newSurfaceRenderer для демонстрации переключения объекта
Для этих объектов устанавливается позиция на экране, тип масштабирования и зеркалирование.
По умолчанию, для отображения видео с камеры устанавливается зеркальная ориентация при помощи метода setMirror(true). Для отображения публикуемого потока и объекта для демонстрации переключения зеркалирование отключается при помощи setMirror(false):
Code Block | ||||
---|---|---|---|---|
| ||||
remoteRenderLayout.setPosition(0, 0, 100, 100); if (mSendAudio.isChecked()) { remoteRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT); audioConstraints = new AudioConstraints(remoteRender.setMirror(false); if (mUseFEC.isChecked()) {remoteRender.requestLayout(); localRenderLayout.setPosition(0, 0, audioConstraints.setUseFEC(true100, 100); }localRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT); if (mUseStereo.isChecked()) {localRender.setMirror(true); audioConstraints.setUseStereo(truelocalRender.requestLayout(); } switchRenderLayout.setPosition(0, 0, 100, 100); if (!mDefaultPublishAudioBitrate.isChecked() && mDefaultPublishAudioBitratenewSurfaceRenderer.getTextsetZOrderMediaOverlay().length() > 0) { true); newSurfaceRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT); audioConstraints newSurfaceRenderer.setBitrate(Integer.parseInt(mPublishAudioBitrate.getText().toString()))setMirror(true); } } 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); } |
4. Локальное тестирование камеры и микрофона
Flashphoner.getLocalMediaAccess() код
Методу передаются:
- настройки аудио и видео, заданные пользователем
- локальный объект SurfaceViewRenderer localRenderer для вывода изображения с выбранной камеры
Code Block | ||||
---|---|---|---|---|
| ||||
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();
soundMeter.getTimer().scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
runOnUiThread(new Runnable() {
@Override
public void run() {
String text = "Level: " + Math.floor(soundMeter.getAmplitude() * 10);
mMicLevel.setText(text);
}
});
}
}, 0, 300);
Log.i(TAG, "Permission has been granted by user");
}
break; |
5. Создание сессии
Flashphoner.createSession() код
Методу передается объект SessionOptions со следующими параметрами
- URL WCS-сервера
- SurfaceViewRenderer localRenderer, который будет использоваться для отображения видео с камеры
- SurfaceViewRenderer remoteRenderer, который будет использоваться для воспроизведения опубликованного видеопотока
Code Block | ||||
---|---|---|---|---|
| ||||
SessionOptions sessionOptions = new SessionOptions(url);
sessionOptions.setLocalRenderer(localRender);
sessionOptions.setRemoteRenderer(remoteRender);
/**
* Session for connection to WCS server is created with method createSession().
*/
session = Flashphoner.createSession(sessionOptions); |
6. Подключение к серверу.
Session.connect(). код
Code Block | ||
---|---|---|
| ||
session.connect(new Connection()); |
7. Получение от сервера события, подтверждающего успешное соединение.
session.onConnected() код
Code Block | ||||
---|---|---|---|---|
| ||||
@Override public void onConnected(final Connection connection) { runOnUiThread(new RunnablenewSurfaceRenderer.requestLayout(); |
В данном случае, при выборе фронтальной камеры изображение с камеры выглядит нормально, но публикуется зеркальным. При выборе тыловой камеры изображение с камеры будет выглядеть зеркальным, а публикуемый поток будет иметь нормальную ориентацию (см. скриншоты приложения выше).
4. Настройка параметров аудио и видео, заданных пользователем
AudioConstraints, VideoConstraints code
Code Block | ||||
---|---|---|---|---|
| ||||
@NonNull private Constraints getConstraints() { AudioConstraints audioConstraints = @Overridenull; public void run(if (mSendAudio.isChecked()) { audioConstraints = new mStartButton.setText(R.string.action_stopAudioConstraints(); if mStartButton.setTag(R.string.action_stop); (mUseFEC.isChecked()) { mStartButtonaudioConstraints.setEnabledsetUseFEC(true); mTestButton.setEnabled(false);} if mStatusView.setText(connectionmUseStereo.getStatusisChecked());; { /**audioConstraints.setUseStereo(true); } * The options for the stream to publish are set. * The stream name is passed when StreamOptions object is created.if (!mDefaultPublishAudioBitrate.isChecked() && mDefaultPublishAudioBitrate.getText().length() > 0) { audioConstraints.setBitrate(Integer.parseInt(mPublishAudioBitrate.getText().toString())); } } * VideoConstraints object isVideoConstraints usedvideoConstraints to= setnull; the source camera, FPS and resolution.if (mSendVideo.isChecked()) { videoConstraints = new VideoConstraints(); * Stream constraints are set with method StreamOptions.setConstraints(). videoConstraints.setCameraId(((MediaDevice) mCameraSpinner.getSpinner().getSelectedItem()).getId()); if (mCameraFPS.getText().length() > 0) */{ StreamOptions streamOptions = new StreamOptions(streamName videoConstraints.setVideoFps(Integer.parseInt(mCameraFPS.getText().toString())); } Constraints constraints = getConstraints(); if (mWidth.getText().length() > 0 streamOptions.setConstraints(constraints); && mHeight.getText().length() > 0) { String[] stripCodec = {(String) mStripStreamerCodec.getSpinner videoConstraints.setResolution(Integer.parseInt(mWidth.getText().getSelectedItemtoString()}; ), streamOptions.setStripCodecs(stripCodecInteger.parseInt(mHeight.getText().toString())); } /** if (!mDefaultPublishVideoBitrate.isChecked() && mPublishVideoBitrate.getText().length() > 0) { * Stream is created with method Session.createStream(). videoConstraints.setBitrate(Integer.parseInt(mPublishVideoBitrate.getText().toString())); } */} return new publishStream = session.createStream(streamOptions); Constraints(audioConstraints, videoConstraints); } |
5. Локальное тестирование камеры и микрофона
Flashphoner.getLocalMediaAccess() code
Методу передаются:
- настройки аудио и видео, заданные пользователем
- локальный объект SurfaceViewRenderer localRenderer для вывода изображения с выбранной камеры
Code Block | ||||
---|---|---|---|---|
| ||||
case TEST_REQUEST_CODE: { if (grantResults.length == ...0 || } }); } |
8. Создание потока и подготовка к публикации
session.createStream() код
Code Block | ||||
---|---|---|---|---|
| ||||
publishStream = session.createStream(streamOptions); if (mMuteAudio.isChecked()) { publishStream.muteAudio(); } if (mMuteVideo.isChecked()) { publishStream.muteVideo(); } ... ActivityCompat.requestPermissions(MediaDevicesActivity.this, grantResults[0] != PackageManager.PERMISSION_GRANTED || grantResults[1] != PackageManager.PERMISSION_GRANTED) { new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA}, PUBLISH_REQUEST_CODE); |
9. Публикация потока
Stream.publish() код
Code Block | ||||
---|---|---|---|---|
| ||||
case PUBLISH_REQUEST_CODE: Log.i(TAG, "Permission has been denied by user"); } else { if (grantResults.length == 0 || Flashphoner.getLocalMediaAccess(getConstraints(), localRender); mTestButton.setText(R.string.action_release); grantResults[0] != PackageManager.PERMISSION_GRANTED || mTestButton.setTag(R.string.action_release); mStartButton.setEnabled(false); grantResults[1] != PackageManager.PERMISSION_GRANTED) { soundMeter = mStartButton.setEnabled(falsenew SoundMeter(); mTestButtonsoundMeter.setEnabledstart(false); session.disconnect();... Log.i(TAG, "Permission has been deniedgranted by user"); } else { /** * Method Stream.publish() is called to publish stream. */ publishStream.publish(); Log.i(TAG, "Permission has been granted by user"); } break; } |
...
break; |
6. Создание сессии
Flashphoner.createSession() code
Методу передается объект SessionOptions со следующими параметрами
- URL WCS-сервера
- SurfaceViewRenderer localRenderer, который будет использоваться для отображения видео с камеры
- SurfaceViewRenderer remoteRenderer, который будет использоваться для воспроизведения опубликованного видеопотока
Code Block | ||||
---|---|---|---|---|
| ||||
SessionOptions sessionOptions = new SessionOptions(url);
sessionOptions.setLocalRenderer(localRender);
sessionOptions.setRemoteRenderer(remoteRender);
/**
* Session for connection to WCS server is created with method createSession().
*/
session = Flashphoner.createSession(sessionOptions); |
7. Подключение к серверу.
Session.connect(). code
Code Block | ||||
---|---|---|---|---|
| ||||
session.connect(new Connection()); |
8. Получение от сервера события, подтверждающего успешную публикацию потока
StreamStatusEvent PUBLISHING код
При получении данного события создается превью-видеопоток при помощи Session.createStream() и вызывается Stream.play() для его воспроизведения.успешное соединение.
session.onConnected() code
Code Block | ||||
---|---|---|---|---|
| ||||
publishStream.on(new StreamStatusEvent() { @Override public void onStreamStatusonConnected(final Stream stream, final StreamStatus streamStatusConnection connection) { runOnUiThread(new Runnable() { @Override public void run() { if (StreamStatus.PUBLISHING.equals(streamStatus)) {mStartButton.setText(R.string.action_stop); /** * The options for the stream to play are set.mStartButton.setTag(R.string.action_stop); mStartButton.setEnabled(true); mTestButton.setEnabled(false); * The stream name is passed when StreamOptions object is createdmStatusView.setText(connection.getStatus());; ... } }); } |
9. Создание потока и подготовка к публикации
session.createStream() code
Code Block | ||||
---|---|---|---|---|
| ||||
publishStream = session.createStream(streamOptions); if (mMuteAudio.isChecked()) { publishStream.muteAudio(); } if */(mMuteVideo.isChecked()) { publishStream.muteVideo(); } ... ActivityCompat.requestPermissions(MediaDevicesActivity.this, StreamOptions streamOptions = new StreamOptions(streamName); new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA}, PUBLISH_REQUEST_CODE); |
10. Публикация потока
Stream.publish() code
Code Block | ||||
---|---|---|---|---|
| ||||
case PUBLISH_REQUEST_CODE: { streamOptions.setConstraints(new Constraints(mReceiveAudio.isChecked(), mReceiveVideo.isChecked())); if (grantResults.length == 0 || grantResults[0] != PackageManager.PERMISSION_GRANTED || VideoConstraints videoConstraints = null; grantResults[1] != PackageManager.PERMISSION_GRANTED) { mStartButton.setEnabled(false); if (mReceiveVideomTestButton.isCheckedsetEnabled(false)) {; session.disconnect(); Log.i(TAG, "Permission has been denied videoConstraints = new VideoConstraints(by user"); } else { /** if (!mDefaultPlayResolution.isChecked() && mPlayWidth.getText().length() > 0 && mPlayHeight.getText().length() > 0) {* Method Stream.publish() is called to publish stream. */ publishStream.publish(); Log.i(TAG, "Permission has been granted by user"); } videoConstraints.setResolution(Integer.parseInt(mPlayWidth.getText().toString()),break; } |
11. Получение от сервера события, подтверждающего успешную публикацию потока
StreamStatusEvent PUBLISHING code
При получении данного события создается превью-видеопоток при помощи Session.createStream() и вызывается Stream.play() для его воспроизведения.
Code Block | ||||
---|---|---|---|---|
| ||||
publishStream.on(new StreamStatusEvent() { @Override public void onStreamStatus(final Stream stream, final StreamStatus streamStatus) { Integer.parseInt(mPlayHeight.getText().toString()));runOnUiThread(new Runnable() { @Override } public void run() { if (!mDefaultPlayBitrate.isChecked() && mPlayBitrate.getText().length() > 0StreamStatus.PUBLISHING.equals(streamStatus)) { /** videoConstraints.setBitrate(Integer.parseInt(mPlayBitrate.getText().toString())); * The options for the stream to play are }set. if (!mDefaultPlayQuality.isChecked() && mPlayQuality.getText().length() > 0) { * The stream name is passed when StreamOptions object is created. videoConstraints.setQuality(Integer.parseInt(mPlayQuality.getText().toString()));*/ StreamOptions streamOptions = new }StreamOptions(streamName); }streamOptions.setConstraints(new Constraints(mReceiveAudio.isChecked(), mReceiveVideo.isChecked())); AudioConstraintsVideoConstraints audioConstraintsvideoConstraints = null; if (mReceiveAudiomReceiveVideo.isChecked()) { audioConstraintsvideoConstraints = new AudioConstraintsVideoConstraints(); ... } streamOptions.setConstraints(new Constraints(audioConstraints, videoConstraints))AudioConstraints audioConstraints = null; String[] stripCodec = {(String) mStripPlayerCodec.getSpinner().getSelectedItem()};if (mReceiveAudio.isChecked()) { streamOptions.setStripCodecs(stripCodec); audioConstraints = new AudioConstraints(); /** } * Stream is created with method Session.createStream().streamOptions.setConstraints(new Constraints(audioConstraints, videoConstraints)); String[] stripCodec */ = {(String) mStripPlayerCodec.getSpinner().getSelectedItem()}; playStream = sessionstreamOptions.createStreamsetStripCodecs(streamOptionsstripCodec); /** * Callback function for stream status change Stream is addedcreated towith display the statusmethod Session.createStream(). */ playStream = session.on(new StreamStatusEvent() {createStream(streamOptions); @Override... /** public void onStreamStatus(final Stream stream, final StreamStatus streamStatus) { * Method Stream.play() is called to start playback of the stream. runOnUiThread(new Runnable() { */ @OverrideplayStream.play(); if (mSendVideo.isChecked()) public void run() { mSwitchCameraButton.setEnabled(true); if (!StreamStatus.PLAYING.equals(streamStatus)) {mSwitchRendererButton.setEnabled(true); } else { Log.e(TAG, "Can not playpublish stream " + stream.getName() + " " + streamStatus); } mStatusView.setText(streamStatus.toString()); } }); } }); |
12. Переключение камеры во время трансляции
Stream.switchCamera() code
Code Block | ||||
---|---|---|---|---|
| ||||
mSwitchCameraButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) }{ if (publishStream != null) { mStatusView.setText(streamStatus.toString())mSwitchCameraButton.setEnabled(false); publishStream.switchCamera(new CameraSwitchHandler() { }@Override public void onCameraSwitchDone(boolean var1) { }); runOnUiThread(new Runnable() { } @Override }); /** public void run() { * Method Stream.play() is called to start playback of the streammSwitchCameraButton.setEnabled(true); */} playStream.play(}); if (mSendVideo.isChecked()) } @Override mSwitchCameraButton.setEnabled(true); public void onCameraSwitchError(String var1) { mSwitchRendererButton.setEnabled(true); } else runOnUiThread(new Runnable() { Log.e(TAG, "Can not publish stream "@Override + stream.getName() + " " + streamStatus); } public void run() { mStatusView.setText(streamStatus.toString()); } }mSwitchCameraButton.setEnabled(true); } }); |
11. Переключение камеры во время трансляции
Stream.switchCamera() код
Code Block | ||||
---|---|---|---|---|
| ||||
mSwitchCameraButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { } if (publishStream != null) { mSwitchCameraButton.setEnabled(false}); publishStream.switchCamera(new CameraSwitchHandler() {} }); @Override} } }); |
13. Переключение объекта для отображения видеопотока во время трансляции
Stream.switchRenderer() code
Code Block | ||||
---|---|---|---|---|
| ||||
mSwitchRendererButton.setOnClickListener(new OnClickListener() { @Override public void onCameraSwitchDoneonClick(booleanView var1v) { runOnUiThread(new Runnableif (spinner.getSelectedItemId() == 0){ if (isSwitchRemoteRenderer) { @Override playStream.switchRenderer(remoteRender); isSwitchRemoteRenderer public void run() {= false; } if mSwitchCameraButton.setEnabled(true);(!isSwitchLocalRenderer) { publishStream.switchRenderer(newSurfaceRenderer); } isSwitchLocalRenderer = true; }); } else { } publishStream.switchRenderer(localRender); @Override isSwitchLocalRenderer = false; public void onCameraSwitchError(String var1) { } } else { runOnUiThread(newif Runnable(isSwitchLocalRenderer) { publishStream.switchRenderer(localRender); @Override isSwitchLocalRenderer = false; public void} run() { if (!isSwitchRemoteRenderer) { mSwitchCameraButtonplayStream.setEnabledswitchRenderer(truenewSurfaceRenderer); isSwitchRemoteRenderer = true; } } else { } playStream.switchRenderer(remoteRender); isSwitchRemoteRenderer }= false; }); } } }); |
12. Переключение объекта для отображения видеопотока во время трансляции
Stream.switchRenderer() код14. Управление звуком при помощи аппаратных кнопок
Flashphoner.setVolume() code
Code Block | ||||
---|---|---|---|---|
| ||||
mSwitchRendererButton.setOnClickListener(new OnClickListener() { @Override public voidboolean onClick(View vonKeyDown(int keyCode, KeyEvent event) { int if (spinner.getSelectedItemIdcurrentVolume = Flashphoner.getVolume(); == 0){ switch (keyCode) { if (isSwitchRemoteRenderer) { case KeyEvent.KEYCODE_VOLUME_DOWN: playStream.switchRenderer(remoteRender); if (currentVolume == 1) { isSwitchRemoteRenderer = false; } Flashphoner.setVolume(0); if (!isSwitchLocalRenderer) {} publishStreammPlayVolume.switchRenderersetProgress(newSurfaceRenderercurrentVolume-1); isSwitchLocalRenderer = truebreak; } else {case KeyEvent.KEYCODE_VOLUME_UP: publishStream.switchRenderer(localRender); if (currentVolume == 0) { isSwitchLocalRenderer = false; Flashphoner.setVolume(1); } } else { if (isSwitchLocalRenderer) {mPlayVolume.setProgress(currentVolume+1); publishStream.switchRenderer(localRender)break; } isSwitchLocalRenderer = falsereturn super.onKeyDown(keyCode, event); } |
15. Использование внешнего динамика телефона
Flashphoner.getAudioManager().isSpeakerphoneOn(), Flashphoner.getAudioManager().setUseSpeakerPhone() code
Code Block | ||||
---|---|---|---|---|
| ||||
} mSpeakerPhone = if (!isSwitchRemoteRenderer) {(CheckBox) findViewById(R.id.use_speakerphone); playStream.switchRenderer(newSurfaceRenderermSpeakerPhone.setChecked(Flashphoner.getAudioManager().getAudioManager().isSpeakerphoneOn()); isSwitchRemoteRenderer = true;mSpeakerPhone.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { }@Override else { public void onCheckedChanged(CompoundButton buttonView, boolean playStream.switchRenderer(remoteRender);isChecked) { isSwitchRemoteRenderer = falseFlashphoner.getAudioManager().setUseSpeakerPhone(isChecked); } } } }); |
1316. Закрытие соединения.
Code Block | ||||
---|---|---|---|---|
| ||||
mStartButton.setEnabled(false); mTestButton.setEnabled(false); /** * Connection to WCS server is closed with method Session.disconnect(). */ session.disconnect(); |
1417. Получение события, подтверждающего разъединение.
session.onDisconnection() код code
Code Block | ||||
---|---|---|---|---|
| ||||
@Override public void onDisconnection(final Connection connection) { runOnUiThread(new Runnable() { @Override public void run() { mStartButton.setText(R.string.action_start); mStartButton.setTag(R.string.action_start); mStartButton.setEnabled(true); mSwitchCameraButton.setEnabled(false); mSwitchRendererButton.setEnabled(false); mStatusView.setText(connection.getStatus()); mTestButton.setEnabled(true); } }); } |