238 lines
9.3 KiB
Java
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();
|
|
});
|
|
}
|
|
}
|