diff --git a/pom.xml b/pom.xml index ae541c4b..9bb2fc09 100644 --- a/pom.xml +++ b/pom.xml @@ -42,6 +42,7 @@ 1.4.11 2.0.9 2.0 + 4.3.1 @@ -196,6 +197,18 @@ ${xtaudio.version} + + com.github.hypfvieh + dbus-java-core + ${dbus.java.version} + + + + com.github.hypfvieh + dbus-java-transport-junixsocket + ${dbus.java.version} + + diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 9b2b9027..feefb6e1 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -16,6 +16,7 @@ requires java.net.http; requires org.slf4j; requires ch.qos.logback.classic; + requires org.freedesktop.dbus; opens org.dpsoftware to javafx.fxml, javafx.web; opens org.dpsoftware.gui to javafx.fxml, javafx.web; diff --git a/src/main/java/org/dpsoftware/FireflyLuciferin.java b/src/main/java/org/dpsoftware/FireflyLuciferin.java index 473e127a..d03e8b2c 100644 --- a/src/main/java/org/dpsoftware/FireflyLuciferin.java +++ b/src/main/java/org/dpsoftware/FireflyLuciferin.java @@ -272,7 +272,7 @@ public void start(Stage stage) throws Exception { if (MainSingleton.getInstance().config.getRuntimeLogLevel().equals("DEBUG")) { MainSingleton.getInstance().guiManager.showSettingsDialog(true); } - NativeExecutor.isWayland(); +// NativeExecutor.isWayland(); } /** @@ -285,6 +285,7 @@ private void launchGrabberAndConsumers() throws AWTException { // Desktop Duplication API producers if ((MainSingleton.getInstance().config.getCaptureMethod().equals(Configuration.CaptureMethod.DDUPL.name())) || (MainSingleton.getInstance().config.getCaptureMethod().equals(Configuration.CaptureMethod.XIMAGESRC.name())) + || (MainSingleton.getInstance().config.getCaptureMethod().equals(Configuration.CaptureMethod.PIPEWIREXDG.name())) || (MainSingleton.getInstance().config.getCaptureMethod().equals(Configuration.CaptureMethod.AVFVIDEOSRC.name()))) { grabberManager.launchAdvancedGrabber(imageProcessor); } else { // Standard Producers diff --git a/src/main/java/org/dpsoftware/config/Configuration.java b/src/main/java/org/dpsoftware/config/Configuration.java index ee9b52a9..55680a70 100644 --- a/src/main/java/org/dpsoftware/config/Configuration.java +++ b/src/main/java/org/dpsoftware/config/Configuration.java @@ -189,6 +189,7 @@ public enum CaptureMethod { WinAPI, DDUPL, XIMAGESRC, + PIPEWIREXDG, AVFVIDEOSRC } diff --git a/src/main/java/org/dpsoftware/config/Constants.java b/src/main/java/org/dpsoftware/config/Constants.java index 62592d26..10f28198 100644 --- a/src/main/java/org/dpsoftware/config/Constants.java +++ b/src/main/java/org/dpsoftware/config/Constants.java @@ -495,6 +495,7 @@ public class Constants { public static final String GSTREAMER_PIPELINE_WINDOWS_HARDWARE_HANDLE_SM = "d3d11screencapturesrc monitor-handle={0} ! d3d11convert ! d3d11download"; public static final String GSTREAMER_PIPELINE_WINDOWS_HARDWARE_HANDLE = "d3d11screencapturesrc monitor-handle={0} ! d3d11convert"; public static final String GSTREAMER_PIPELINE_LINUX = "ximagesrc startx={0} endx={1} starty={2} endy={3} use-damage=0 ! videoscale ! videoconvert"; + public static final String GSTREAMER_PIPELINE_PIPEWIREXDG = "pipewiresrc fd={1} path={2} ! videorate ! videoscale ! videoconvert"; public static final String GSTREAMER_PIPELINE_MAC = "avfvideosrc capture-screen=true ! videoscale ! videoconvert"; public static final String FRAMERATE_PLACEHOLDER = "framerate=FRAMERATE_PLACEHOLDER/1,"; public static final String FPS_PLACEHOLDER = "FRAMERATE_PLACEHOLDER"; diff --git a/src/main/java/org/dpsoftware/grabber/DbusScreenCast.java b/src/main/java/org/dpsoftware/grabber/DbusScreenCast.java new file mode 100644 index 00000000..427e78fa --- /dev/null +++ b/src/main/java/org/dpsoftware/grabber/DbusScreenCast.java @@ -0,0 +1,29 @@ +package org.dpsoftware.grabber; + +import org.freedesktop.dbus.DBusPath; +import org.freedesktop.dbus.FileDescriptor; +import org.freedesktop.dbus.annotations.DBusInterfaceName; +import org.freedesktop.dbus.annotations.DBusProperty; +import org.freedesktop.dbus.annotations.DBusProperty.Access; +import org.freedesktop.dbus.interfaces.DBusInterface; +import org.freedesktop.dbus.types.UInt32; +import org.freedesktop.dbus.types.Variant; + +import java.util.Map; + +/** + * Auto-generated class. + */ +@DBusProperty(name = "AvailableSourceTypes", type = UInt32.class, access = Access.READ) +@DBusProperty(name = "AvailableCursorModes", type = UInt32.class, access = Access.READ) +@DBusProperty(name = "version", type = UInt32.class, access = Access.READ) +@DBusInterfaceName(value = "org.freedesktop.portal.ScreenCast") +public interface DbusScreenCast extends DBusInterface { + + + public DBusPath CreateSession(Map> options); + public DBusPath SelectSources(DBusPath sessionHandle, Map> options); + public DBusPath Start(DBusPath sessionHandle, String parentWindow, Map> options); + public FileDescriptor OpenPipeWireRemote(DBusPath sessionHandle, Map> options); + +} diff --git a/src/main/java/org/dpsoftware/gui/controllers/MiscTabController.java b/src/main/java/org/dpsoftware/gui/controllers/MiscTabController.java index 152e5e2c..a4c07a45 100644 --- a/src/main/java/org/dpsoftware/gui/controllers/MiscTabController.java +++ b/src/main/java/org/dpsoftware/gui/controllers/MiscTabController.java @@ -301,7 +301,8 @@ public void initValuesFromSettingsFile(Configuration currentConfig, boolean upda } frameInsertion.setDisable((!currentConfig.getCaptureMethod().equals(Configuration.CaptureMethod.DDUPL.name())) && (!currentConfig.getCaptureMethod().equals(Configuration.CaptureMethod.XIMAGESRC.name())) - && (!currentConfig.getCaptureMethod().equals(Configuration.CaptureMethod.AVFVIDEOSRC.name()))); + && (!currentConfig.getCaptureMethod().equals(Configuration.CaptureMethod.PIPEWIREXDG.name()) + && (!currentConfig.getCaptureMethod().equals(Configuration.CaptureMethod.AVFVIDEOSRC.name())))); gamma.setValue(String.valueOf(MainSingleton.getInstance().config.getGamma())); colorMode.setValue(Enums.ColorMode.values()[MainSingleton.getInstance().config.getColorMode() - 1].getI18n()); if (!MainSingleton.getInstance().config.getDesiredFramerate().equals(Enums.Framerate.UNLOCKED.getBaseI18n())) { diff --git a/src/main/java/org/dpsoftware/gui/controllers/ModeTabController.java b/src/main/java/org/dpsoftware/gui/controllers/ModeTabController.java index 5be054e0..c8ed34d5 100644 --- a/src/main/java/org/dpsoftware/gui/controllers/ModeTabController.java +++ b/src/main/java/org/dpsoftware/gui/controllers/ModeTabController.java @@ -94,7 +94,7 @@ public void injectSettingsController(SettingsController settingsController) { @FXML protected void initialize() { if (NativeExecutor.isLinux()) { - captureMethod.getItems().addAll(Configuration.CaptureMethod.XIMAGESRC); + captureMethod.getItems().addAll(Configuration.CaptureMethod.XIMAGESRC, Configuration.CaptureMethod.PIPEWIREXDG); } for (Enums.Algo al : Enums.Algo.values()) { algo.getItems().add(al.getI18n()); @@ -158,7 +158,11 @@ void initDefaultValues() { } else if (NativeExecutor.isMac()) { captureMethod.setValue(Configuration.CaptureMethod.DDUPL); } else { - captureMethod.setValue(Configuration.CaptureMethod.XIMAGESRC); + if (System.getenv("XDG_SESSION_TYPE").equalsIgnoreCase("wayland")) { + captureMethod.setValue(Configuration.CaptureMethod.PIPEWIREXDG); + } else { + captureMethod.setValue(Configuration.CaptureMethod.XIMAGESRC); + } } } } diff --git a/src/main/java/org/dpsoftware/gui/controllers/SettingsController.java b/src/main/java/org/dpsoftware/gui/controllers/SettingsController.java index 4aa0cd30..816f470c 100644 --- a/src/main/java/org/dpsoftware/gui/controllers/SettingsController.java +++ b/src/main/java/org/dpsoftware/gui/controllers/SettingsController.java @@ -526,6 +526,8 @@ void setCaptureMethod(Configuration config) { } else { if (modeTabController.captureMethod.getValue() == Configuration.CaptureMethod.XIMAGESRC) { config.setCaptureMethod(Configuration.CaptureMethod.XIMAGESRC.name()); + } else if (modeTabController.captureMethod.getValue() == Configuration.CaptureMethod.PIPEWIREXDG) { + config.setCaptureMethod(Configuration.CaptureMethod.PIPEWIREXDG.name()); } } } @@ -753,6 +755,8 @@ public void initOutputDeviceChooser(boolean initCaptureMethod) { modeTabController.captureMethod.getItems().addAll(Configuration.CaptureMethod.DDUPL, Configuration.CaptureMethod.WinAPI, Configuration.CaptureMethod.CPU); } else if (NativeExecutor.isMac()) { modeTabController.captureMethod.getItems().addAll(Configuration.CaptureMethod.AVFVIDEOSRC); + } else { + modeTabController.captureMethod.getItems().addAll(Configuration.CaptureMethod.XIMAGESRC, Configuration.CaptureMethod.PIPEWIREXDG); } } if (NativeExecutor.isWindows()) { diff --git a/src/main/java/org/dpsoftware/managers/PipelineManager.java b/src/main/java/org/dpsoftware/managers/PipelineManager.java index e4954b7b..45ceef45 100644 --- a/src/main/java/org/dpsoftware/managers/PipelineManager.java +++ b/src/main/java/org/dpsoftware/managers/PipelineManager.java @@ -21,6 +21,7 @@ */ package org.dpsoftware.managers; +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.dpsoftware.MainSingleton; import org.dpsoftware.audio.*; @@ -28,6 +29,7 @@ import org.dpsoftware.config.Constants; import org.dpsoftware.config.Enums; import org.dpsoftware.config.LocalizedEnum; +import org.dpsoftware.grabber.DbusScreenCast; import org.dpsoftware.grabber.GrabberSingleton; import org.dpsoftware.gui.GuiSingleton; import org.dpsoftware.gui.elements.DisplayInfo; @@ -40,10 +42,22 @@ import org.dpsoftware.network.MessageClient; import org.dpsoftware.network.NetworkSingleton; import org.dpsoftware.utilities.CommonUtility; +import org.freedesktop.dbus.DBusMap; +import org.freedesktop.dbus.DBusMatchRule; +import org.freedesktop.dbus.DBusPath; +import org.freedesktop.dbus.FileDescriptor; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; +import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.types.UInt32; +import org.freedesktop.dbus.types.Variant; import java.awt.*; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -58,21 +72,90 @@ public class PipelineManager { UpgradeManager upgradeManager = new UpgradeManager(); private ScheduledExecutorService scheduledExecutorService; + + record XdgStreamDetails(Integer streamId, FileDescriptor fileDescriptor) {} + /** + * Uses D-BUS to get the XDG ScreenCast stream ID & pipewire filedescriptor + * + * @throws RuntimeException on any concurrency or D-BUS issues + * @return XDG ScreenCast stream details containing the ID from org.freedesktop.portal.ScreenCast:Start and + * FileDescriptor from org.freedesktop.portal.ScreenCast:OpenPipeWireRemote + */ + @SneakyThrows + public static XdgStreamDetails getXdgStreamDetails() { + CompletableFuture sessionHandleMaybe = new CompletableFuture<>(); + CompletableFuture streamIdMaybe = new CompletableFuture<>(); + + DBusConnection dBusConnection = DBusConnectionBuilder.forSessionBus().build(); // cannot free/close this for the duration of the capture + + DbusScreenCast screenCastIface = dBusConnection.getRemoteObject("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", DbusScreenCast.class); + String handleToken = UUID.randomUUID().toString().replaceAll("-", ""); + + DBusMatchRule matchRule = new DBusMatchRule("signal", "org.freedesktop.portal.Request", "Response"); + dBusConnection.addGenericSigHandler(matchRule, signal -> { + try { + if (signal.getParameters().length == 2 // verify amount of arguments + && signal.getParameters()[0] instanceof UInt32 // verify argument types + && signal.getParameters()[1] instanceof DBusMap + && ((UInt32)signal.getParameters()[0]).intValue() == 0 // verify success-code + ) { + // parse signal & set appropriate Future as the result + if (((DBusMap) signal.getParameters()[1]).containsKey("session_handle")) { + sessionHandleMaybe.complete((String)(((Variant)((DBusMap) signal.getParameters()[1]).get("session_handle")).getValue())); + } else if (((DBusMap) signal.getParameters()[1]).containsKey("streams")) { + streamIdMaybe.complete(((UInt32)((Object[]) ((List) (((Variant) ((DBusMap) signal.getParameters()[1]).get("streams")).getValue())).get(0))[0]).intValue()); + } + } + } catch (DBusException e) { + throw new RuntimeException(e); // couldn't parse, fail early + } + }); + + screenCastIface.CreateSession(Map.of("session_handle_token", new Variant<>(handleToken))); + DBusPath receivedSessionHandle = new DBusPath(sessionHandleMaybe.get()); + + screenCastIface.SelectSources(receivedSessionHandle, + Map.of( + "multiple", new Variant<>(false), + "types", new Variant<>(new UInt32(1|2)) // bitmask, 1 - screens, 2 - windows + ) + ); + screenCastIface.Start(receivedSessionHandle, "", Collections.emptyMap()); + + return streamIdMaybe.thenApply(streamId -> { + FileDescriptor fileDescriptor = screenCastIface.OpenPipeWireRemote(receivedSessionHandle, Collections.emptyMap()); // block until stream started before calling OpenPipeWireRemote + + return new XdgStreamDetails(streamId, fileDescriptor); + }).get(); + } + /** * Calculate correct Pipeline for Linux * * @return params for Linux Pipeline */ public static String getLinuxPipelineParams() { - // startx{0}, endx{1}, starty{2}, endy{3} - DisplayManager displayManager = new DisplayManager(); - List displayList = displayManager.getDisplayList(); - DisplayInfo monitorInfo = displayList.get(MainSingleton.getInstance().config.getMonitorNumber()); - String gstreamerPipeline = Constants.GSTREAMER_PIPELINE_LINUX - .replace("{0}", String.valueOf((int) (monitorInfo.getMinX() + 1))) - .replace("{1}", String.valueOf((int) (monitorInfo.getMinX() + monitorInfo.getWidth() - 1))) - .replace("{2}", String.valueOf((int) (monitorInfo.getMinY()))) - .replace("{3}", String.valueOf((int) (monitorInfo.getMinY() + monitorInfo.getHeight() - 1))); + String gstreamerPipeline; + + if (MainSingleton.getInstance().config.getCaptureMethod().equals(Configuration.CaptureMethod.PIPEWIREXDG.name())) { + XdgStreamDetails xdgStreamDetails = getXdgStreamDetails(); + + gstreamerPipeline = Constants.GSTREAMER_PIPELINE_PIPEWIREXDG + .replace("{1}", String.valueOf(xdgStreamDetails.fileDescriptor.getIntFileDescriptor())) + .replace("{2}", xdgStreamDetails.streamId.toString()); + } else { + // startx{0}, endx{1}, starty{2}, endy{3} + DisplayManager displayManager = new DisplayManager(); + List displayList = displayManager.getDisplayList(); + DisplayInfo monitorInfo = displayList.get(MainSingleton.getInstance().config.getMonitorNumber()); + + gstreamerPipeline = Constants.GSTREAMER_PIPELINE_LINUX + .replace("{0}", String.valueOf((int) (monitorInfo.getMinX() + 1))) + .replace("{1}", String.valueOf((int) (monitorInfo.getMinX() + monitorInfo.getWidth() - 1))) + .replace("{2}", String.valueOf((int) (monitorInfo.getMinY()))) + .replace("{3}", String.valueOf((int) (monitorInfo.getMinY() + monitorInfo.getHeight() - 1))); + } + log.info(gstreamerPipeline); return gstreamerPipeline; } @@ -332,6 +415,7 @@ public void stopCapturePipeline() { } if (GrabberSingleton.getInstance().pipe != null && ((MainSingleton.getInstance().config.getCaptureMethod().equals(Configuration.CaptureMethod.DDUPL.name())) || (MainSingleton.getInstance().config.getCaptureMethod().equals(Configuration.CaptureMethod.XIMAGESRC.name())) + || (MainSingleton.getInstance().config.getCaptureMethod().equals(Configuration.CaptureMethod.PIPEWIREXDG.name())) || (MainSingleton.getInstance().config.getCaptureMethod().equals(Configuration.CaptureMethod.AVFVIDEOSRC.name())))) { GrabberSingleton.getInstance().pipe.stop(); }