Чтобы приложение Android не выгружалось из памяти устройства, и публикация видео не останавливалась при сворачивании приложения, необходимо устанавливать соединение с WCS сервером и публиковать видео из сервиса, который должен быть запущен из Activity приложения. В свою очередь, чтобы и сервис не был выгружен из памяти, необходимо создать уведомление, которое должно находиться в панели уведомлений, пока сервис работает. Рассмотрим модификацию примера Android Two Way Streaming
1. Создание сервиса при нажатии кнопки 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"); } } } } |
2. Создание сессии и публикация потока при старте сервиса
@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; } |
3. Создание уведомления
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); } |
4. Остановка сервиса при нажатии кнопки 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; } ... } }); |
5. Остановка публикации при остановке сервиса
@Override public void onDestroy() { super.onDestroy(); publishStream.stop(); Toast.makeText(this, "Stop service", Toast.LENGTH_SHORT).show(); stopForeground(true); } |
Полный код примера модификации файла 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
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; } } |
Полный код примера модификации манифеста приложения
<?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> |
1. При публикации потока из сервиса невозможно отображение локального видео в приложении
2. В приведенном примере, при закрытии приложения из списка запущенных приложений, сервис также остановится.