Публикация потока из приложения Android в фоновом режиме¶
Описание¶
Чтобы приложение Android не выгружалось из памяти устройства, и публикация видео не останавливалась при сворачивании приложения, необходимо устанавливать соединение с WCS сервером и публиковать видео из сервиса, который должен быть запущен из Activity приложения. В свою очередь, чтобы и сервис не был выгружен из памяти, необходимо создать уведомление, которое должно находиться в панели уведомлений, пока сервис работает. Рассмотрим пример модификации исходного кода приложения Android Two Way Streaming
Пример модификации исходного кода приложения¶
-
Создание сервиса при нажатии кнопки
Publish
и успешном получении доступа к камере и микрофону
@Override public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { switch (requestCode) { case PUBLISH_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 { mPublishButton.setEnabled(false); ... Intent intent = new Intent(StreamingMinActivity.this, TestService.class); intent.putExtra("url", mWcsUrlView.getText().toString()); intent.putExtra("streamName", mPublishStreamView.getText().toString()); startService(intent); Log.i(TAG, "Permission has been granted by user"); } } } }
-
Создание сессии и публикация потока при старте сервиса
@Override public int onStartCommand(Intent intent, int flags, int startId) { SessionOptions sessionOptions = new SessionOptions(intent.getStringExtra("url")); Session session = Flashphoner.createSession(sessionOptions); session.connect(new Connection()); StreamOptions streamOptions = new StreamOptions(intent.getStringExtra("streamName")); Stream publishStream = session.createStream(streamOptions); publishStream.publish(); Toast.makeText(this, "Start service", Toast.LENGTH_SHORT).show(); return START_STICKY; }
-
Создание уведомления
private void showNotification() { Intent notificationIntent = new Intent(this, StreamingMinActivity.class); notificationIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); int iconId = R.mipmap.ic_launcher; int uniqueCode = new Random().nextInt(Integer.MAX_VALUE); Notification notification = new NotificationCompat.Builder(this) .setSmallIcon(iconId) .setContentText("Started stream") .setContentIntent(pendingIntent).build(); startForeground(uniqueCode, notification); }
-
Остановка сервиса при нажатии кнопки
Unpublish
mPublishButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { if (mPublishButton.getTag() == null || Integer.valueOf(R.string.action_publish).equals(mPublishButton.getTag())) { ... } else { mPublishButton.setEnabled(false); ... stopService(new Intent(StreamingMinActivity.this, TestService.class)); publishStream = null; } ... } });
-
Остановка публикации при остановке сервиса
Полный код примера модификации файла StreamingMinActivity.java
StreamingMinActivity.java
package com.flashphoner.wcsexample.streaming_min;
import android.Manifest;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import com.flashphoner.fpwcsapi.Flashphoner;
import com.flashphoner.fpwcsapi.bean.Connection;
import com.flashphoner.fpwcsapi.bean.Data;
import com.flashphoner.fpwcsapi.bean.StreamStatus;
import com.flashphoner.fpwcsapi.bean.StreamStatusInfo;
import com.flashphoner.fpwcsapi.layout.PercentFrameLayout;
import com.flashphoner.fpwcsapi.session.Session;
import com.flashphoner.fpwcsapi.session.SessionEvent;
import com.flashphoner.fpwcsapi.session.SessionOptions;
import com.flashphoner.fpwcsapi.session.Stream;
import com.flashphoner.fpwcsapi.session.StreamOptions;
import com.flashphoner.fpwcsapi.session.StreamStatusEvent;
import junit.framework.Test;
import org.webrtc.PeerConnection;
import org.webrtc.RendererCommon;
import org.webrtc.SurfaceViewRenderer;
import java.util.ArrayList;
import java.util.List;
/**
* Example with streamer and player.
* Demonstrates how to publish a video stream while playing another one.
*/
public class StreamingMinActivity extends AppCompatActivity {
private static String TAG = StreamingMinActivity.class.getName();
private static final int PUBLISH_REQUEST_CODE = 100;
// UI references.
private EditText mWcsUrlView;
private TextView mConnectStatus;
private Button mConnectButton;
private EditText mPublishStreamView;
private TextView mPublishStatus;
private Button mPublishButton;
private EditText mPlayStreamView;
private TextView mPlayStatus;
private Button mPlayButton;
private Session session;
private Stream publishStream;
private Stream playStream;
private SurfaceViewRenderer localRender;
private SurfaceViewRenderer remoteRender;
private PercentFrameLayout localRenderLayout;
private PercentFrameLayout remoteRenderLayout;
private SessionOptions sessionOptions;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_streaming_min);
/**
* Initialization of the API.
*/
Flashphoner.init(this);
mWcsUrlView = (EditText) findViewById(R.id.wcs_url);
SharedPreferences sharedPref = this.getPreferences(Context.MODE_PRIVATE);
mWcsUrlView.setText(sharedPref.getString("wcs_url", getString(R.string.wcs_url)));
mConnectStatus = (TextView) findViewById(R.id.connect_status);
mConnectButton = (Button) findViewById(R.id.connect_button);
/**
* Connection to server will be established when Connect button is clicked.
*/
mConnectButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
if (mConnectButton.getTag() == null || Integer.valueOf(R.string.action_connect).equals(mConnectButton.getTag())) {
/**
* The options for connection session are set.
* WCS server URL is passed when SessionOptions object is created.
* SurfaceViewRenderer to be used to display video from the camera is set with method SessionOptions.setLocalRenderer().
* SurfaceViewRenderer to be used to display video of the played stream is set with method SessionOptions.setRemoteRenderer().
*/
sessionOptions = new SessionOptions(mWcsUrlView.getText().toString());
sessionOptions.setLocalRenderer(localRender);
sessionOptions.setRemoteRenderer(remoteRender);
/**
* Uncomment this code to use your own RTCConfiguration. For example, you can use custom TURN server
*/
//List<PeerConnection.IceServer> iceServers = new ArrayList<>();
//iceServers.add(new PeerConnection.IceServer("turn:your.turn-server.com:443?transport=tcp","username","passw0rd"));
//PeerConnection.RTCConfiguration customConfig = new PeerConnection.RTCConfiguration(iceServers);
//sessionOptions.setMediaOptions(customConfig);
/**
* Session for connection to WCS server is created with method createSession().
*/
session = Flashphoner.createSession(sessionOptions);
/**
* Callback functions for session status events are added to make appropriate changes in controls of the interface when connection is established and closed.
*/
session.on(new SessionEvent() {
@Override
public void onAppData(Data data) {
}
@Override
public void onConnected(final Connection connection) {
runOnUiThread(new Runnable() {
@Override
public void run() {
mConnectButton.setText(R.string.action_disconnect);
mConnectButton.setTag(R.string.action_disconnect);
mConnectButton.setEnabled(true);
mConnectStatus.setText(connection.getStatus());
mPublishButton.setEnabled(true);
mPlayButton.setEnabled(true);
}
});
}
@Override
public void onRegistered(Connection connection) {
}
@Override
public void onDisconnection(final Connection connection) {
runOnUiThread(new Runnable() {
@Override
public void run() {
mConnectButton.setText(R.string.action_connect);
mConnectButton.setTag(R.string.action_connect);
mConnectButton.setEnabled(true);
mPublishButton.setText(R.string.action_publish);
mPublishButton.setTag(R.string.action_publish);
mPublishButton.setEnabled(false);
mPlayButton.setText(R.string.action_play);
mPlayButton.setTag(R.string.action_play);
mPlayButton.setEnabled(false);
mConnectStatus.setText(connection.getStatus());
mPublishStatus.setText("");
mPlayStatus.setText("");
}
});
}
});
mConnectButton.setEnabled(false);
/**
* Connection to WCS server is established with method Session.connect().
*/
session.connect(new Connection());
SharedPreferences sharedPref = StreamingMinActivity.this.getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putString("wcs_url", mWcsUrlView.getText().toString());
editor.apply();
} else {
mConnectButton.setEnabled(false);
/**
* Connection to WCS server is closed with method Session.disconnect().
*/
session.disconnect();
}
View currentFocus = getCurrentFocus();
if (currentFocus != null) {
InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.hideSoftInputFromWindow(currentFocus.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
}
}
});
mPublishStreamView = (EditText) findViewById(R.id.publish_stream);
mPublishStreamView.setText(sharedPref.getString("publish_stream", getString(R.string.default_publish_name)));
mPublishStatus = (TextView) findViewById(R.id.publish_status);
mPublishButton = (Button) findViewById(R.id.publish_button);
/**
* Stream will be published when Publish button is clicked.
*/
mPublishButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
if (mPublishButton.getTag() == null || Integer.valueOf(R.string.action_publish).equals(mPublishButton.getTag())) {
ActivityCompat.requestPermissions(StreamingMinActivity.this,
new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA},
PUBLISH_REQUEST_CODE);
SharedPreferences sharedPref = StreamingMinActivity.this.getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putString("publish_stream", mPublishStreamView.getText().toString());
editor.apply();
} else {
mPublishButton.setEnabled(false);
/**
* Method Stream.stop() is called to unpublish the stream.
*/
//publishStream.stop();
stopService(new Intent(StreamingMinActivity.this, TestService.class));
publishStream = null;
}
View currentFocus = getCurrentFocus();
if (currentFocus != null) {
InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.hideSoftInputFromWindow(currentFocus.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
}
}
});
mPlayStreamView = (EditText) findViewById(R.id.play_stream);
mPlayStreamView.setText(sharedPref.getString("play_stream", getString(R.string.default_play_name)));
mPlayStatus = (TextView) findViewById(R.id.play_status);
mPlayButton = (Button) findViewById(R.id.play_button);
/**
* Stream playback will be started when Play button is clicked.
*/
mPlayButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
mPlayButton.setEnabled(false);
if (mPlayButton.getTag() == null || Integer.valueOf(R.string.action_play).equals(mPlayButton.getTag())) {
/**
* The options for the stream to play are set.
* The stream name is passed when StreamOptions object is created.
*/
StreamOptions streamOptions = new StreamOptions(mPlayStreamView.getText().toString());
/**
* Stream is created with method Session.createStream().
*/
playStream = session.createStream(streamOptions);
/**
* Callback function for stream status change is added to make appropriate changes in controls of the interface when playing.
*/
playStream.on(new StreamStatusEvent() {
@Override
public void onStreamStatus(final Stream stream, final StreamStatus streamStatus) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (StreamStatus.PLAYING.equals(streamStatus)) {
mPlayButton.setText(R.string.action_stop);
mPlayButton.setTag(R.string.action_stop);
} else if (StreamStatus.NOT_ENOUGH_BANDWIDTH.equals(streamStatus)) {
Log.w(TAG, "Not enough bandwidth stream " + stream.getName() + ", consider using lower video resolution or bitrate. " +
"Bandwidth " + (Math.round(stream.getNetworkBandwidth() / 1000)) + " " +
"bitrate " + (Math.round(stream.getRemoteBitrate() / 1000)));
} else {
mPlayButton.setText(R.string.action_play);
mPlayButton.setTag(R.string.action_play);
}
mPlayButton.setEnabled(true);
if (StreamStatus.FAILED.equals(streamStatus)){
switch (stream.getInfo()){
case StreamStatusInfo.SESSION_DOES_NOT_EXIST:
mPlayStatus.setText(streamStatus+": Actual session does not exist");
break;
case StreamStatusInfo.STOPPED_BY_PUBLISHER_STOP:
mPlayStatus.setText(streamStatus+": Related publisher stopped its stream or lost connection");
break;
case StreamStatusInfo.SESSION_NOT_READY:
mPlayStatus.setText(streamStatus+": Session is not initialized or terminated on play ordinary stream");
break;
case StreamStatusInfo.RTSP_STREAM_NOT_FOUND:
mPlayStatus.setText(streamStatus+": Rtsp stream not found where agent received '404-Not Found'");
break;
case StreamStatusInfo.FAILED_TO_CONNECT_TO_RTSP_STREAM:
mPlayStatus.setText(streamStatus+": Failed to connect to rtsp stream");
break;
case StreamStatusInfo.FILE_NOT_FOUND:
mPlayStatus.setText(streamStatus+": File does not exist, check filename");
break;
case StreamStatusInfo.FILE_HAS_WRONG_FORMAT:
mPlayStatus.setText(streamStatus+": File has wrong format on play vod, this format is not supported");
break;
default:{
mPlayStatus.setText(stream.getInfo());
}
}
} else {
mPlayStatus.setText(streamStatus.toString());
}
}
});
}
});
/**
* Method Stream.play() is called to start playback of the stream.
*/
playStream.play();
SharedPreferences sharedPref = StreamingMinActivity.this.getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putString("play_stream", mPlayStreamView.getText().toString());
editor.apply();
} else {
/**
* Method Stream.stop() is called to stop playback of the stream.
*/
playStream.stop();
playStream = null;
}
View currentFocus = getCurrentFocus();
if (currentFocus != null) {
InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.hideSoftInputFromWindow(currentFocus.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
}
}
});
localRender = (SurfaceViewRenderer) findViewById(R.id.local_video_view);
remoteRender = (SurfaceViewRenderer) findViewById(R.id.remote_video_view);
localRenderLayout = (PercentFrameLayout) findViewById(R.id.local_video_layout);
remoteRenderLayout = (PercentFrameLayout) findViewById(R.id.remote_video_layout);
localRender.setZOrderMediaOverlay(true);
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();
}
@Override
protected void onResume() {
super.onResume();
try {
localRender.init(Flashphoner.context, null);
} catch (IllegalStateException e) {
//ignore
}
try {
remoteRender.init(Flashphoner.context, null);
} catch (IllegalStateException e) {
//ignore
}
}
@Override
protected void onPause() {
super.onPause();
localRender.release();
remoteRender.release();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (session != null) {
session.disconnect();
}
}
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String permissions[], @NonNull int[] grantResults) {
switch (requestCode) {
case PUBLISH_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 {
mPublishButton.setEnabled(false);
/**
* The options for the stream to publish are set.
* The stream name is passed when StreamOptions object is created.
*/
StreamOptions streamOptions = new StreamOptions(mPublishStreamView.getText().toString());
/**
* Uncomment this code to use case WebRTC-as-RTMP. Stream will be republished to your rtmpUrl
*/
//streamOptions.setRtmpUrl("rtmp://192.168.1.100:1935/live2");
/**
* Stream is created with method Session.createStream().
*/
publishStream = session.createStream(streamOptions);
/**
* Callback function for stream status change is added to make appropriate changes in controls of the interface when publishing.
*/
publishStream.on(new StreamStatusEvent() {
@Override
public void onStreamStatus(final Stream stream, final StreamStatus streamStatus) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (StreamStatus.PUBLISHING.equals(streamStatus)) {
mPublishButton.setText(R.string.action_unpublish);
mPublishButton.setTag(R.string.action_unpublish);
} else {
mPublishButton.setText(R.string.action_publish);
mPublishButton.setTag(R.string.action_publish);
}
mPublishButton.setEnabled(true);
if (StreamStatus.FAILED.equals(streamStatus)){
switch (stream.getInfo()){
case StreamStatusInfo.STREAM_NAME_ALREADY_IN_USE:
mPublishStatus.setText(streamStatus+": Server already has a publish stream with the same name, try using different one");
break;
default:{
mPlayStatus.setText(stream.getInfo());
}
}
} else {
mPublishStatus.setText(streamStatus.toString());
}
}
});
}
});
/**
* Method Stream.publish() is called to publish stream.
*/
//publishStream.publish();
Intent intent = new Intent(StreamingMinActivity.this, TestService.class);
intent.putExtra("url", mWcsUrlView.getText().toString());
intent.putExtra("streamName", mPublishStreamView.getText().toString());
startService(intent);
Log.i(TAG, "Permission has been granted by user");
}
}
}
}
}
Полный код примера файла реализации сервиса TestService.java
TestService.java
package com.flashphoner.wcsexample.streaming_min;
import android.app.Activity;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.widget.EditText;
import android.widget.Toast;
import com.flashphoner.fpwcsapi.Flashphoner;
import com.flashphoner.fpwcsapi.bean.Connection;
import com.flashphoner.fpwcsapi.session.Session;
import com.flashphoner.fpwcsapi.session.SessionOptions;
import com.flashphoner.fpwcsapi.session.Stream;
import com.flashphoner.fpwcsapi.session.StreamOptions;
import junit.framework.Test;
import java.util.ArrayList;
import java.util.Random;
public class TestService extends Service {
private static Stream publishStream;
private static Session session;
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
//throw new UnsupportedOperationException("Not yet implemented");
return null;
}
@Override
public void onCreate() {
super.onCreate();
showNotification();
Toast.makeText(this, "Create service",
Toast.LENGTH_SHORT).show();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
SessionOptions sessionOptions = new SessionOptions(intent.getStringExtra("url"));
Session session = Flashphoner.createSession(sessionOptions);
session.connect(new Connection());
StreamOptions streamOptions = new StreamOptions(intent.getStringExtra("streamName"));
Stream publishStream = session.createStream(streamOptions);
publishStream.publish();
Toast.makeText(this, "Start service", Toast.LENGTH_SHORT).show();
return START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
publishStream.stop();
Toast.makeText(this, "Stop service",
Toast.LENGTH_SHORT).show();
stopForeground(true);
}
private void showNotification() {
Intent notificationIntent = new Intent(this, StreamingMinActivity.class);
notificationIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
int iconId = R.mipmap.ic_launcher;
int uniqueCode = new Random().nextInt(Integer.MAX_VALUE);
Notification notification = new NotificationCompat.Builder(this)
.setSmallIcon(iconId)
.setContentText("Started stream")
.setContentIntent(pendingIntent).build();
startForeground(uniqueCode, notification);
}
public static void setSession(Session session) {
TestService.session = session;
}
public static void setPublishStream(Stream publishStream) {
TestService.publishStream = publishStream;
}
}
Полный код примера модификации манифеста приложения
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.flashphoner.wcsexample.streaming_min">
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".StreamingMinActivity"
android:configChanges="orientation|screenSize"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".TestService"
android:enabled="true"
android:exported="true">
</service>
</application>
</manifest>
Известные ограничения¶
-
При публикации потока из сервиса невозможно отображение локального видео в приложении
-
В приведенном примере, при закрытии приложения из списка запущенных приложений, сервис также остановится.