DroidCast/app/src/main/java/com/jerryxiao/droidcast/ScreenCapService.java
2023-08-18 15:16:37 +08:00

238 lines
9.3 KiB
Java

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<Consumer<Boolean>> connectionStateCallbacks = new HashSet<>();
private final HashSet<Consumer<String>> 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<Boolean> booleanConsumer : this.connectionStateCallbacks) {
try {
booleanConsumer.accept(connected);
}
catch (Throwable ignored) {}
}
}
}
protected void addConnectionStateCallback(Consumer<Boolean> runnable) {
synchronized (this.connectionStateCallbacks) {
this.connectionStateCallbacks.add(runnable);
}
}
protected void addInfoDisplayCallback(Consumer<String> stringConsumer) {
synchronized (this.infoDisplayCallbacks) {
this.infoDisplayCallbacks.add(stringConsumer);
}
}
protected void removeConnectionStateCallback(Consumer<Boolean> runnable) {
synchronized (this.connectionStateCallbacks) {
this.connectionStateCallbacks.remove(runnable);
}
}
protected void removeInfoDisplayCallback(Consumer<String> stringConsumer) {
synchronized (this.infoDisplayCallbacks) {
this.infoDisplayCallbacks.remove(stringConsumer);
}
}
private void setInfoDisplay(String t) {
synchronized (this.infoDisplayCallbacks) {
this.infoDisplayText = t;
for (Consumer<String> 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();
});
}
}