package com.jerryxiao.droidcast; import static androidx.core.app.NotificationCompat.PRIORITY_LOW; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.ServiceInfo; import android.graphics.Point; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.util.Log; import android.view.WindowManager; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.core.app.NotificationCompat; import androidx.preference.PreferenceManager; import com.pedro.encoder.utils.CodecUtil; import com.pedro.rtmp.utils.ConnectCheckerRtmp; import com.pedro.rtplibrary.rtmp.RtmpDisplay; import java.util.HashSet; import java.util.function.Consumer; public class ScreenCapService extends Service { private static final String TAG = "ScreenCapService"; private static final int RESOLUTION_GAP = 100000; private final HashSet> connectionStateCallbacks = new HashSet<>(); private final HashSet> infoDisplayCallbacks = new HashSet<>(); private SharedPreferences prefs; private boolean connected = false; public boolean isConnected() { return connected; } protected class MBinder extends Binder { public ScreenCapService getService() { return ScreenCapService.this; } } private final IBinder mBinder = new MBinder(); private RtmpDisplay screenCapDisplay; public RtmpDisplay getScreenCapDisplay() { return this.screenCapDisplay; } private String infoDisplayText = ""; public String getInfoDisplayText() { return infoDisplayText; } public IBinder onBind(Intent intent) { return this.mBinder; } public void onCreate() { super.onCreate(); Log.d(TAG, "service created"); this.prefs = PreferenceManager.getDefaultSharedPreferences(this); this.infoDisplayText = getString(R.string.status_disconnected); } public void onDestroy() { super.onDestroy(); Log.d(TAG, "service destroyed"); } protected void createDisplay() { boolean use_gl = prefs.getBoolean("use_gl", true); Log.d(TAG, String.format("creating display use_gl=%b", use_gl)); this.screenCapDisplay = new RtmpDisplay(this, use_gl, new ConnectCheckerRtmp() { public void onAuthErrorRtmp() { String text = getString(R.string.status_auth_error); showToast(text); setInfoDisplay(text); ScreenCapService.this.stop(); } public void onAuthSuccessRtmp() { setInfoDisplay(getString(R.string.status_auth_success)); } public void onConnectionFailedRtmp(@NonNull String reason) { String text = getString(R.string.status_connection_failed_format, reason); showToast(text); setInfoDisplay(text); ScreenCapService.this.stop(); } public void onConnectionStartedRtmp(@NonNull String rtmpUrl) { setInfoDisplay(getString(R.string.status_connecting, rtmpUrl)); ScreenCapService.this.connectionStateChange(true); } public void onConnectionSuccessRtmp() { setInfoDisplay(getString(R.string.status_connected)); } public void onDisconnectRtmp() { setInfoDisplay(getString(R.string.status_disconnected)); ScreenCapService.this.stop(); } public void onNewBitrateRtmp(long bitrate) { setInfoDisplay(getString(R.string.status_bitrate_format, bitrate)); } }); } protected void startCapture(String server) { if (this.connected) { Log.d(TAG, "not starting capture"); return; } Log.d(TAG, "start capture"); startService(new Intent(this, ScreenCapService.class)); this.connected = true; NotificationChannel notificationChannel = new NotificationChannel("notifications", "Notifications", NotificationManager.IMPORTANCE_LOW); notificationChannel.setDescription("Notifications"); getSystemService(NotificationManager.class).createNotificationChannel(notificationChannel); NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, "notifications"); notificationBuilder.setChannelId("notifications").setContentTitle("DroidCast").setContentText("Streaming") .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), PendingIntent.FLAG_IMMUTABLE)) .setSmallIcon(R.drawable.ic_launcher_foreground).setOngoing(true).setPriority(PRIORITY_LOW); this.startForeground(1, notificationBuilder.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION); screenCapDisplay.setForce(prefs.getBoolean("software_video_encoder", false) ? CodecUtil.Force.SOFTWARE : CodecUtil.Force.FIRST_COMPATIBLE_FOUND, prefs.getBoolean("software_audio_encoder", false) ? CodecUtil.Force.SOFTWARE : CodecUtil.Force.FIRST_COMPATIBLE_FOUND); screenCapDisplay.setWriteChunkSize(prefs.getInt("write_chunk_size_int", 1024)); screenCapDisplay.setReTries(prefs.getInt("retries_int", 0)); screenCapDisplay.resizeCache(prefs.getInt("cache_size_int", 120)); screenCapDisplay.setLogs(prefs.getBoolean("enable_lib_logs", true)); if (prefs.getBoolean("use_gl", true) && prefs.getBoolean("use_force_render", true)) { screenCapDisplay.getGlInterface().setForceRender(true); } int video_combined = 192001080; try { video_combined = Integer.parseInt(prefs.getString("resolution", "match_device")); } catch (NumberFormatException e) { Point resolution = new Point(); ((WindowManager)getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getRealSize(resolution); assert resolution.x < RESOLUTION_GAP && resolution.y < RESOLUTION_GAP; video_combined = resolution.x * RESOLUTION_GAP + resolution.y; } int video_width = video_combined / RESOLUTION_GAP; int video_height = video_combined % RESOLUTION_GAP; if (prefs.getBoolean("screen_landscape", true) != video_width > video_height) { int tmp = video_height; video_height = video_width; video_width = tmp; } Log.d(TAG, String.format("capture width %d height %d", video_width, video_height)); if (this.screenCapDisplay.prepareInternalAudio(Integer.parseInt(prefs.getString("audio_bitrate", "131072")), 48000, prefs.getBoolean("stereo_audio", true), false, false) && this.screenCapDisplay.prepareVideo(video_width, video_height, Integer.parseInt(prefs.getString("fps", "30")), Integer.parseInt(prefs.getString("video_bitrate", "10485760")), 0, 320, -1, -1, 2)) { this.screenCapDisplay.startStream(server); } else { Log.d(TAG, "cannot prepare video audio"); String text = getString(R.string.error_prepare_video_audio); setInfoDisplay(text); showToast(text); this.stop(); } } private void stop() { Log.d(TAG, "stop called"); this.connected = false; this.stopForeground(true); this.stopSelf(); this.connectionStateChange(false); } private void connectionStateChange(boolean connected) { synchronized (this.connectionStateCallbacks) { for (Consumer booleanConsumer : this.connectionStateCallbacks) { try { booleanConsumer.accept(connected); } catch (Throwable ignored) {} } } } protected void addConnectionStateCallback(Consumer runnable) { synchronized (this.connectionStateCallbacks) { this.connectionStateCallbacks.add(runnable); } } protected void addInfoDisplayCallback(Consumer stringConsumer) { synchronized (this.infoDisplayCallbacks) { this.infoDisplayCallbacks.add(stringConsumer); } } protected void removeConnectionStateCallback(Consumer runnable) { synchronized (this.connectionStateCallbacks) { this.connectionStateCallbacks.remove(runnable); } } protected void removeInfoDisplayCallback(Consumer stringConsumer) { synchronized (this.infoDisplayCallbacks) { this.infoDisplayCallbacks.remove(stringConsumer); } } private void setInfoDisplay(String t) { synchronized (this.infoDisplayCallbacks) { this.infoDisplayText = t; for (Consumer stringConsumer : this.infoDisplayCallbacks) { try { stringConsumer.accept(t); } catch (Throwable ignored) {} } } } private void showToast(String text) { (new Handler(Looper.getMainLooper())).post(() -> { Toast.makeText(ScreenCapService.this, text, Toast.LENGTH_SHORT).show(); }); } }