Entrega v1 aula: shell com abas, PITIUPI_HOME e plugins de referência.

Consolida núcleo (presença, peer actions, MessageDispatcher), chat e screencast na galeria, documentação PT-BR e testes JUnit; remove mpChat do catálogo em favor do chat unificado.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Gabriel Felipe Souza SantosandCursor committed 2026-10-27 11:08:21 -03:00
1 parent b10c196fc9
commit eeb1936f6d
178 files changed
+7641 -1481

No files matched your search

@@ -0,0 +1 @@
pitiupi.screenCast.plugin.ScreenCastPlugin
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../../pitiupi/dist/pitiupi.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
@@ -0,0 +1,52 @@
/*
* Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
* Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
*/
package pitiupi.screenCast.GUI;
import java.util.List;
import pitiupi.models.Menus.MulticastMenus.MulticastActionMenu;
import pitiupi.models.Menus.MulticastMenus.MulticastMenu;
import pitiupi.models.Menus.MulticastMenus.MulticastSubMenu;
/**
*
* @author flavio
*/
public class ScreenCastMenu extends MulticastSubMenu {
private final ScreenCastShowMenu showMenu;
public ScreenCastMenu(ScreenCastPanel screenCastPanel) {
this.showMenu = new ScreenCastShowMenu(screenCastPanel);
}
@Override
public String getText() {
return "Screen Cast";
}
@Override
public List<MulticastMenu> getMenus() {
return List.of(showMenu);
}
private static final class ScreenCastShowMenu extends MulticastActionMenu {
private final ScreenCastPanel screenCastPanel;
ScreenCastShowMenu(ScreenCastPanel screenCastPanel) {
this.screenCastPanel = screenCastPanel;
}
@Override
public String getText() {
return "ScreenCast Window";
}
@Override
public void onClick() {
screenCastPanel.requestFocusInWindow();
}
}
}
@@ -0,0 +1,25 @@
package pitiupi.screenCast.GUI;
import pitiupi.presentation.shell.PluginSessionKeys;
/**
* Abas em que o usuário pode ligar envio de tela (outbound).
*/
final class ScreenCastOutboundTabs {
private ScreenCastOutboundTabs() {
}
static boolean isOutboundShareTabKey(String key) {
if (key == null || key.isBlank()) {
return false;
}
if (PluginSessionKeys.PEER_DRAFT.equals(key)) {
return false;
}
if (key.startsWith("view:")) {
return false;
}
return PluginSessionKeys.ROOM.equals(key) || key.startsWith("peer:");
}
}
@@ -0,0 +1,591 @@
package pitiupi.screenCast.GUI;
import pitiupi.application.messaging.MessageDispatcher;
import pitiupi.application.messaging.PluginTransportCatalog;
import pitiupi.application.messaging.SendTarget;
import pitiupi.application.messaging.TransportMode;
import pitiupi.application.presence.UserPeerView;
import pitiupi.control.ConnectionManager;
import pitiupi.presentation.shell.PeerDraftPanel;
import pitiupi.presentation.shell.PluginRoutingPanel;
import pitiupi.presentation.shell.PluginSessionKeys;
import pitiupi.presentation.shell.PluginSessionWorkspace;
import pitiupi.presentation.shell.SessionPeerLabels;
import pitiupi.presentation.shell.SessionTabBar;
import pitiupi.presentation.shell.ui.ShellUi;
import pitiupi.screenCast.control.AlarmClock;
import pitiupi.screenCast.control.ImageTransformation;
import pitiupi.screenCast.control.ScreenCaptureDiagnostics;
import pitiupi.screenCast.control.ScreenShot;
import pitiupi.screenCast.net.ScreenCastMessage;
import java.awt.AWTException;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.FlowLayout;
import java.awt.event.HierarchyEvent;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JToggleButton;
import javax.swing.SwingUtilities;
/**
* Painel de compartilhamento de tela.
* Backlog UX: prévia dividida (recebendo + enviando) na mesma aba peer; multi-share simultâneo.
*/
public class ScreenCastPanel extends JPanel {
public static final int TIME = 1000;
private static final int MAX_WIDTH = 800;
private static final int MAX_HEIGHT = 600;
private static final String CARD_DRAFT = "__draft__";
private static final String IDLE_ROOM_HTML =
"<html><p>Ative <b>Compartilhamento da sua tela</b> para enviar para todos na rede (multicast).</p></html>";
private static final String IDLE_PEER_HTML =
"<html><p>Ative o compartilhamento para enviar só para este peer (unicast).</p></html>";
private AlarmClock alarmClock;
private Timer timer;
private float compressRatio;
private final ConnectionManager connectionManager;
private final PluginSessionWorkspace workspace;
private final PluginRoutingPanel routingPanel;
private final SessionTabBar sessionTabs;
private final JToggleButton shareToggle = new JToggleButton("Compartilhamento da sua tela (desligado)");
private final Map<String, Boolean> sharingByTabKey = new HashMap<>();
private boolean suppressShareToggleEvents;
private String shareContextTabKey;
private String lastSessionKey = PluginSessionKeys.ROOM;
private final Map<String, String> peerIpsByViewKey = new HashMap<>();
private final Map<String, String> peerDisplayNames = new HashMap<>();
private final Map<String, ScreenCastReceiveScope> receiveScopes = new HashMap<>();
private final CardLayout bodyLayout = new CardLayout();
private final JPanel bodyCards = new JPanel(bodyLayout);
private final Map<String, JPanel> bodyPanels = new HashMap<>();
private PeerDraftPanel draftPanel;
public ScreenCastPanel(ConnectionManager connectionManager) {
this.connectionManager = connectionManager;
this.compressRatio = 0.55f;
this.routingPanel = new PluginRoutingPanel(
PluginTransportCatalog.PluginKind.SCREEN_CAST,
"<html><i>Receba pela lista lateral (notificação) ou use + Nova para um IP. "
+ "A aba Todos envia em multicast; abas de peer usam unicast.</i></html>",
false
);
this.workspace = new PluginSessionWorkspace(routingPanel, buildShareToolbar());
this.sessionTabs = workspace.getSessionTabBar();
sessionTabs.setCloseFallbackKey(PluginSessionKeys.ROOM);
initComponents();
}
private JPanel buildShareToolbar() {
shareToggle.setFocusPainted(false);
shareToggle.addActionListener(e -> {
if (suppressShareToggleEvents) {
return;
}
String key = sessionTabs.selectedKey();
if (!ScreenCastOutboundTabs.isOutboundShareTabKey(key)) {
return;
}
sharingByTabKey.put(key, shareToggle.isSelected());
updateShareToggleLabel();
applySharingForActiveTab();
});
JPanel shareRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 4));
shareRow.add(shareToggle);
return shareRow;
}
private void initComponents() {
setLayout(new BorderLayout());
draftPanel = new PeerDraftPanel(
"Nova sessão por IP",
"Informe o IP para enviar ou receber unicast. O nome na aba atualiza quando o peer for conhecido.",
this::commitDraftIp
);
bodyCards.add(draftPanel, CARD_DRAFT);
workspace.getBodyHost().add(bodyCards, BorderLayout.CENTER);
add(workspace, BorderLayout.CENTER);
timer = new Timer();
addHierarchyListener(e -> {
if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0 && isShowing()) {
onSessionTabSelected(sessionTabs.selectedKey());
}
});
initSessionTabs();
}
private void initSessionTabs() {
sessionTabs.addOrSelectTab(
PluginSessionKeys.ROOM,
"Todos",
"Transmissão para todos na rede (multicast)",
new JPanel(),
false
);
sessionTabs.setOnTabSelected(this::onSessionTabSelected);
sessionTabs.setOnAddPeerRequested(this::openDraftPeerTab);
sessionTabs.setOnTabClosed(this::onSessionTabClosed);
ensureBodyPanel(PluginSessionKeys.ROOM);
setBodyMessage(PluginSessionKeys.ROOM, ShellUi.mutedLabel(IDLE_ROOM_HTML));
sessionTabs.selectTab(PluginSessionKeys.ROOM);
}
private void onSessionTabClosed(String key) {
if (PluginSessionKeys.PEER_DRAFT.equals(key)) {
if (draftPanel != null) {
draftPanel.clearIpField();
}
SwingUtilities.invokeLater(this::restoreSessionAfterDraftClosed);
}
sharingByTabKey.remove(key);
}
private void restoreSessionAfterDraftClosed() {
String restore = lastSessionKey != null && sessionTabs.hasTab(lastSessionKey)
? lastSessionKey
: PluginSessionKeys.ROOM;
if (PluginSessionKeys.PEER_DRAFT.equals(restore)) {
restore = PluginSessionKeys.ROOM;
}
sessionTabs.selectTabSilently(restore);
onSessionTabSelected(restore);
}
private void onSessionTabSelected(String key) {
persistShareStateForContextTab();
if (key != null && !PluginSessionKeys.PEER_DRAFT.equals(key)) {
lastSessionKey = key;
}
if (PluginSessionKeys.PEER_DRAFT.equals(key)) {
cancelShareTimer();
applyShareToggleForTab(key);
showBodyCard(CARD_DRAFT);
draftPanel.focusIpField();
return;
}
if (key == null) {
key = PluginSessionKeys.ROOM;
}
showBodyCard(key);
applyRoutingForSessionKey(key);
applyShareToggleForTab(key);
applySharingForActiveTab();
if (!isSharingForTab(key)) {
refreshOutboundBodyForTab(key);
}
}
private void persistShareStateForContextTab() {
if (shareContextTabKey != null && ScreenCastOutboundTabs.isOutboundShareTabKey(shareContextTabKey)) {
sharingByTabKey.put(shareContextTabKey, shareToggle.isSelected());
}
}
private void applyShareToggleForTab(String key) {
shareContextTabKey = ScreenCastOutboundTabs.isOutboundShareTabKey(key) ? key : null;
suppressShareToggleEvents = true;
try {
if (shareContextTabKey == null) {
shareToggle.setEnabled(false);
shareToggle.setSelected(false);
} else {
shareToggle.setEnabled(true);
shareToggle.setSelected(sharingByTabKey.getOrDefault(shareContextTabKey, false));
}
updateShareToggleLabel();
} finally {
suppressShareToggleEvents = false;
}
}
private void setShareToggleSelectedSilently(boolean selected) {
suppressShareToggleEvents = true;
try {
shareToggle.setSelected(selected);
updateShareToggleLabel();
} finally {
suppressShareToggleEvents = false;
}
}
private boolean isSharingForTab(String key) {
return ScreenCastOutboundTabs.isOutboundShareTabKey(key)
&& sharingByTabKey.getOrDefault(key, false);
}
private void applySharingForActiveTab() {
cancelShareTimer();
String key = sessionTabs.selectedKey();
if (!ScreenCastOutboundTabs.isOutboundShareTabKey(key)) {
return;
}
boolean on = sharingByTabKey.getOrDefault(key, shareToggle.isSelected());
sharingByTabKey.put(key, on);
if (!on) {
refreshOutboundBodyForTab(key);
return;
}
syncRoutingFromActiveSessionTab();
timer = new Timer();
alarmClock = new AlarmClock(this);
timer.scheduleAtFixedRate(alarmClock, 0, TIME);
String previewKey = previewTabKey();
ensureBodyPanel(previewKey);
setBodyMessage(previewKey, ShellUi.mutedLabel("Capturando tela…"));
showBodyCard(previewKey);
}
private void refreshOutboundBodyForTab(String key) {
if (key == null || PluginSessionKeys.PEER_DRAFT.equals(key) || key.startsWith("view:")) {
return;
}
if (isSharingForTab(key)) {
return;
}
if (PluginSessionKeys.ROOM.equals(key)) {
setBodyMessage(key, ShellUi.mutedLabel(IDLE_ROOM_HTML));
} else if (key.startsWith("peer:")) {
setBodyMessage(key, ShellUi.mutedLabel(IDLE_PEER_HTML));
}
}
private void syncRoutingFromActiveSessionTab() {
applyRoutingForSessionKey(sessionTabs.selectedKey());
}
private void applyRoutingForSessionKey(String key) {
if (key == null) {
routingPanel.bindRoomSession();
routingPanel.setReceiveScopeHint(null);
return;
}
if (PluginSessionKeys.PEER_DRAFT.equals(key)) {
return;
}
if (PluginSessionKeys.ROOM.equals(key)) {
routingPanel.bindRoomSession();
routingPanel.setReceiveScopeHint(null);
return;
}
if (key.startsWith("peer:")) {
String ip = key.substring("peer:".length());
String name = peerDisplayNames.getOrDefault(ip, ip);
routingPanel.bindPeerSession(ip, name);
routingPanel.setReceiveScopeHint(null);
return;
}
if (key.startsWith("view:")) {
String ip = peerIpsByViewKey.get(key);
String title = key.substring("view:".length());
ScreenCastReceiveScope scope = receiveScopes.getOrDefault(key, ScreenCastReceiveScope.BROADCAST);
if (ip != null && !ip.isBlank()) {
routingPanel.bindPeerSession(ip, title, scope.bannerHtml());
} else {
routingPanel.bindPeerSession("", title, scope.bannerHtml());
}
}
}
private void openDraftPeerTab() {
String previous = sessionTabs.selectedKey();
if (previous != null && !PluginSessionKeys.PEER_DRAFT.equals(previous)) {
lastSessionKey = previous;
}
if (!sessionTabs.hasTab(PluginSessionKeys.PEER_DRAFT)) {
sessionTabs.addOrSelectTab(
PluginSessionKeys.PEER_DRAFT,
"Nova conversa",
"Informe o IP do peer",
new JPanel(),
true
);
} else {
sessionTabs.selectTab(PluginSessionKeys.PEER_DRAFT);
}
showBodyCard(CARD_DRAFT);
draftPanel.focusIpField();
}
private void commitDraftIp(String ip) {
if (draftPanel != null) {
draftPanel.clearIpField();
}
if (sessionTabs.hasTab(PluginSessionKeys.PEER_DRAFT)) {
sessionTabs.closeTab(PluginSessionKeys.PEER_DRAFT);
}
openPeerTab(ip, ip);
}
private void openPeerTab(String ip, String displayName) {
String canonical = canonicalPeerIp(ip);
String resolved = SessionPeerLabels.resolveDisplayName(connectionManager, canonical, displayName);
String key = PluginSessionKeys.peer(canonical);
peerDisplayNames.put(canonical, resolved);
ensureBodyPanel(key);
setBodyMessage(key, ShellUi.mutedLabel(IDLE_PEER_HTML));
if (sessionTabs.hasTab(key)) {
sessionTabs.setTabTitle(key, resolved, canonical);
sessionTabs.selectTab(key);
} else {
sessionTabs.addOrSelectTab(key, resolved, canonical, new JPanel(), true);
}
routingPanel.bindPeerSession(canonical, resolved);
showBodyCard(key);
}
private static String canonicalPeerIp(String ip) {
if (ip == null || ip.isBlank()) {
return "";
}
try {
return InetAddress.getByName(ip.trim()).getHostAddress();
} catch (Exception ex) {
return ip.trim();
}
}
public void focusMulticastSharing() {
ensureTodosTab();
sharingByTabKey.put(PluginSessionKeys.ROOM, true);
sessionTabs.selectTab(PluginSessionKeys.ROOM);
routingPanel.bindRoomSession();
routingPanel.selectTransport(TransportMode.MULTICAST_UDP);
setShareToggleSelectedSilently(true);
applySharingForActiveTab();
}
public void focusPeerSharing(UserPeerView peer) {
if (peer == null || peer.isBroadcastTarget() || peer.isSelf()) {
return;
}
peer.getInetAddress().ifPresent(addr -> {
String ip = addr.getHostAddress();
openPeerTab(ip, peer.getDisplayName());
String key = PluginSessionKeys.peer(canonicalPeerIp(ip));
sharingByTabKey.put(key, true);
routingPanel.selectTransport(TransportMode.UNICAST_UDP);
setShareToggleSelectedSilently(true);
applySharingForActiveTab();
});
}
public void focusPeerAddress(UserPeerView peer) {
if (peer == null || peer.isBroadcastTarget() || peer.isSelf()) {
return;
}
peer.getInetAddress().ifPresent(addr -> openPeerTab(addr.getHostAddress(), peer.getDisplayName()));
}
private void ensureTodosTab() {
if (!sessionTabs.hasTab(PluginSessionKeys.ROOM)) {
sessionTabs.addOrSelectTab(
PluginSessionKeys.ROOM,
"Todos",
"Transmissão para todos na rede (multicast)",
new JPanel(),
false
);
ensureBodyPanel(PluginSessionKeys.ROOM);
setBodyMessage(PluginSessionKeys.ROOM, ShellUi.mutedLabel(IDLE_ROOM_HTML));
}
}
private void updateShareToggleLabel() {
shareToggle.setText(shareToggle.isSelected()
? "Compartilhamento da sua tela (ligado)"
: "Compartilhamento da sua tela (desligado)");
}
private void cancelShareTimer() {
if (timer != null) {
timer.cancel();
timer = null;
}
}
public void stopSharing() {
sharingByTabKey.replaceAll((k, v) -> false);
setShareToggleSelectedSilently(false);
cancelShareTimer();
refreshOutboundBodyForTab(sessionTabs.selectedKey());
}
public void sendScreenCastMessage() {
String activeKey = sessionTabs.selectedKey();
if (!isSharingForTab(activeKey)) {
return;
}
syncRoutingFromActiveSessionTab();
try {
BufferedImage image = ScreenShot.takeAShot();
if (image == null || image.getWidth() < 1 || image.getHeight() < 1) {
Logger.getLogger(ScreenCastPanel.class.getName()).log(Level.WARNING, "Captura de tela vazia");
showCaptureWarning();
return;
}
boolean black = ScreenCaptureDiagnostics.isLikelyBlackFrame(image);
BufferedImage rescaled = ImageTransformation.resize(image, MAX_WIDTH, MAX_HEIGHT);
byte[] byteImage = ImageTransformation.compress(rescaled, compressRatio);
if (byteImage == null || byteImage.length < 32) {
return;
}
ScreenCastMessage message = new ScreenCastMessage(connectionManager.getUserName(), byteImage);
PluginRoutingPanel.RoutingSnapshot routing = routingPanel.snapshot();
TransportMode mode = routing.transport();
SendTarget target = resolveSendTarget(routing);
if (target == null || mode == null) {
return;
}
int sent = MessageDispatcher.send(connectionManager, message, target, mode);
if (sent == 0 && mode == TransportMode.MULTICAST_UDP) {
compressRatio = Math.max(0.25f, compressRatio - 0.1f);
}
if (black) {
showCaptureWarning();
} else {
showLocalPreview(rescaled);
}
} catch (AWTException | IOException ex) {
Logger.getLogger(AlarmClock.class.getName()).log(Level.SEVERE, null, ex);
showCaptureWarning();
} catch (Exception ex) {
Logger.getLogger(ScreenCastPanel.class.getName()).log(Level.SEVERE, null, ex);
}
}
private void showCaptureWarning() {
SwingUtilities.invokeLater(() -> {
JLabel label = new JLabel(ScreenCaptureDiagnostics.blackFrameUserMessage());
String key = previewTabKey();
setBodyContent(key, label);
showBodyCard(key);
});
}
private void showLocalPreview(BufferedImage image) {
SwingUtilities.invokeLater(() -> {
String key = previewTabKey();
JLabel label = new JLabel(new ImageIcon(image));
setBodyContent(key, new JScrollPane(label));
showBodyCard(key);
});
}
private SendTarget resolveSendTarget(PluginRoutingPanel.RoutingSnapshot routing) throws Exception {
if (routing.everyoneInRoom()) {
return SendTarget.broadcastAll();
}
String ip = routing.peerIp();
if (ip == null || ip.isEmpty()) {
return null;
}
return SendTarget.singlePeer(InetAddress.getByName(ip));
}
public void receiveScreenCastMessage(ScreenCastMessage message, ScreenCastReceiveScope scope) {
if (isOwnOutboundFrame(message)) {
return;
}
byte[] bytes = message.getImageByte();
String viewKey = "view:" + message.getName();
if (bytes == null || bytes.length < 32) {
SwingUtilities.invokeLater(() -> updateTabError(viewKey, message.getName(), "Frame vazio ou corrompido.", scope));
return;
}
SwingUtilities.invokeLater(() -> {
try {
BufferedImage image = ImageIO.read(new ByteArrayInputStream(bytes));
if (image == null) {
updateTabError(viewKey, message.getName(), "Não foi possível decodificar a imagem.", scope);
return;
}
receiveScopes.put(viewKey, scope != null ? scope : ScreenCastReceiveScope.BROADCAST);
resolveSenderIp(message.getName(), viewKey);
sessionTabs.addOrSelectTab(viewKey, message.getName(), message.getName(), new JPanel(), true);
setBodyContent(viewKey, new JScrollPane(new JLabel(new ImageIcon(image))));
sessionTabs.selectTab(viewKey);
showBodyCard(viewKey);
} catch (IOException ex) {
updateTabError(viewKey, message.getName(), "Erro ao ler imagem: " + ex.getMessage(), scope);
}
});
}
private void resolveSenderIp(String senderName, String viewKey) {
for (UserPeerView view : connectionManager.getPresenceService().listPeerViews()) {
if (view.isBroadcastTarget()) {
continue;
}
if (senderName != null && senderName.equals(view.getDisplayName())) {
view.getInetAddress().ifPresent(addr -> peerIpsByViewKey.put(viewKey, addr.getHostAddress()));
return;
}
}
}
private void updateTabError(String tabKey, String title, String error, ScreenCastReceiveScope scope) {
receiveScopes.put(tabKey, scope != null ? scope : ScreenCastReceiveScope.BROADCAST);
JLabel label = new JLabel("<html><p style='color:#c62828'>" + error + "</p></html>");
setBodyContent(tabKey, label);
sessionTabs.addOrSelectTab(tabKey, title, title, new JPanel(), true);
showBodyCard(tabKey);
}
private void ensureBodyPanel(String key) {
if (!bodyPanels.containsKey(key)) {
JPanel panel = new JPanel(new BorderLayout());
bodyPanels.put(key, panel);
bodyCards.add(panel, key);
}
}
private void setBodyContent(String key, java.awt.Component content) {
ensureBodyPanel(key);
JPanel panel = bodyPanels.get(key);
panel.removeAll();
panel.add(content, BorderLayout.CENTER);
panel.revalidate();
panel.repaint();
}
private void setBodyMessage(String key, JLabel label) {
setBodyContent(key, label);
}
private String previewTabKey() {
String selected = sessionTabs.selectedKey();
if (selected != null && !PluginSessionKeys.PEER_DRAFT.equals(selected)) {
return selected;
}
return PluginSessionKeys.ROOM;
}
private boolean isOwnOutboundFrame(ScreenCastMessage message) {
if (message == null || message.getName() == null) {
return false;
}
String localName = connectionManager.getUserName();
return localName != null && localName.equals(message.getName().trim());
}
private void showBodyCard(String card) {
bodyLayout.show(bodyCards, card);
}
}
@@ -0,0 +1,13 @@
package pitiupi.screenCast.GUI;
public enum ScreenCastReceiveScope {
BROADCAST,
DIRECT;
public String bannerHtml() {
if (this == BROADCAST) {
return "<html><i>Transmissão para todos na rede</i></html>";
}
return "<html><i>Transmissão direcionada a você</i></html>";
}
}
@@ -0,0 +1,18 @@
package pitiupi.screenCast.control;
import java.util.TimerTask;
import pitiupi.screenCast.GUI.ScreenCastPanel;
public class AlarmClock extends TimerTask {
private final ScreenCastPanel screenCastPanel;
public AlarmClock(ScreenCastPanel screenCastPanel) {
this.screenCastPanel = screenCastPanel;
}
@Override
public void run() {
screenCastPanel.sendScreenCastMessage();
}
}
@@ -0,0 +1,69 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package pitiupi.screenCast.control;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Iterator;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
/**
*
* @author flavio
*/
public class ImageTransformation {
public static byte[] toByteArray(BufferedImage image) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, "jpg", baos);
byte[] data = baos.toByteArray();
return data;
}
public static byte[] compress(BufferedImage image, float scale) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
ImageWriter writer = writers.next();
ImageWriteParam param = writer.getDefaultWriteParam();
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(scale);
ImageOutputStream ios = ImageIO.createImageOutputStream(baos);
writer.setOutput(ios);
writer.write(null, new IIOImage(image, null, null), param);
byte[] data = baos.toByteArray();
writer.dispose();
return data;
}
public static BufferedImage resize(BufferedImage img, int maxWidth, int maxHeight) throws IOException {
int scaledWidth = maxWidth;
int scaledHeight = (int) (img.getHeight() * ((double) scaledWidth / img.getWidth()));
if (scaledHeight > maxHeight) {
scaledHeight = maxHeight;
scaledWidth = (int) (img.getWidth() * ((double) scaledHeight / img.getHeight()));
if (scaledWidth > maxWidth) {
scaledWidth = maxWidth;
scaledHeight = maxHeight;
}
}
Image resized = img.getScaledInstance(scaledWidth, scaledHeight, Image.SCALE_SMOOTH);
BufferedImage buffered = new BufferedImage(scaledWidth, scaledHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics = buffered.createGraphics();
graphics.drawImage(resized, 0, 0, null);
graphics.dispose();
return buffered;
}
}
@@ -0,0 +1,74 @@
package pitiupi.screenCast.control;
import java.awt.GraphicsEnvironment;
import java.awt.image.BufferedImage;
import java.util.logging.Level;
import java.util.logging.Logger;
public final class ScreenCaptureDiagnostics {
private static final Logger LOG = Logger.getLogger(ScreenCaptureDiagnostics.class.getName());
private static boolean environmentLogged;
private ScreenCaptureDiagnostics() {
}
public static boolean isLikelyBlackFrame(BufferedImage image) {
if (image == null || image.getWidth() < 8 || image.getHeight() < 8) {
return true;
}
int stepX = Math.max(1, image.getWidth() / 16);
int stepY = Math.max(1, image.getHeight() / 16);
int dark = 0;
int samples = 0;
for (int y = 0; y < image.getHeight(); y += stepY) {
for (int x = 0; x < image.getWidth(); x += stepX) {
int rgb = image.getRGB(x, y);
int r = (rgb >> 16) & 0xff;
int g = (rgb >> 8) & 0xff;
int b = rgb & 0xff;
if (r < 12 && g < 12 && b < 12) {
dark++;
}
samples++;
}
}
return samples > 0 && dark >= samples * 0.92;
}
public static String blackFrameUserMessage() {
logEnvironmentOnce();
String wayland = System.getenv("WAYLAND_DISPLAY");
String display = System.getenv("DISPLAY");
return "<html><p><b>Captura retornou tela preta.</b></p>"
+ "<p>Em WSL/WSLg o <code>java.awt.Robot</code> costuma não capturar o desktop.</p>"
+ "<ul>"
+ "<li>Teste o Pitiupi no <b>Windows nativo</b> (fora do WSL) para compartilhar a tela real.</li>"
+ "<li>No WSL, confira WSLg: <code>WAYLAND_DISPLAY="
+ (wayland != null ? wayland : "—")
+ "</code>, <code>DISPLAY="
+ (display != null ? display : "—")
+ "</code>.</li>"
+ "<li>Se os frames ainda saem pretos, o envio na rede pode ocorrer, mas o preview local não reflete sua tela.</li>"
+ "</ul></html>";
}
private static void logEnvironmentOnce() {
if (environmentLogged) {
return;
}
environmentLogged = true;
try {
GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
LOG.log(Level.INFO, "Screen capture env: {0}, displays={1}, DISPLAY={2}, WAYLAND_DISPLAY={3}",
new Object[]{
env.getClass().getSimpleName(),
env.getScreenDevices().length,
System.getenv("DISPLAY"),
System.getenv("WAYLAND_DISPLAY")
});
} catch (Exception ex) {
LOG.log(Level.WARNING, "Screen capture env unavailable", ex);
}
}
}
@@ -0,0 +1,27 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package pitiupi.screenCast.control;
import java.awt.AWTException;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.io.IOException;
/**
*
* @author flavio
*/
public class ScreenShot {
public static BufferedImage takeAShot() throws AWTException, IOException {
Robot robot = new Robot();
Rectangle screenRect = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
BufferedImage output = robot.createScreenCapture(screenRect);
return output;
}
}
@@ -0,0 +1,55 @@
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package pitiupi.screenCast.net;
import pitiupi.models.Messages.Message;
/**
*
* @author flavio
*/
public class ScreenCastMessage extends Message {
private String name;
private byte[] imageByte;
public ScreenCastMessage() {
}
public ScreenCastMessage(String name, byte[] imageByte) {
this.name = name;
this.imageByte = imageByte;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the imageByte
*/
public byte[] getImageByte() {
return imageByte;
}
/**
* @param imageByte the imageByte to set
*/
public void setImageByte(byte[] imageByte) {
this.imageByte = imageByte;
}
}
@@ -0,0 +1,156 @@
package pitiupi.screenCast.plugin;
import java.util.List;
import pitiupi.api.plugin.PeerActionsRegistrar;
import pitiupi.api.plugin.PluginWorkspace;
import pitiupi.application.activity.PeerActivityInbox;
import pitiupi.application.peeractions.PeerAction;
import pitiupi.application.presence.UserPeerView;
import pitiupi.application.peeractions.PeerActionTarget;
import pitiupi.application.peeractions.PeerTarget;
import pitiupi.control.ConnectionManager;
import pitiupi.control.GuiManager;
import pitiupi.models.Menus.MenuCategory;
import pitiupi.models.Messages.Message;
import pitiupi.models.Plugin;
import pitiupi.screenCast.GUI.ScreenCastMenu;
import pitiupi.screenCast.GUI.ScreenCastPanel;
import pitiupi.screenCast.GUI.ScreenCastReceiveScope;
import pitiupi.screenCast.net.ScreenCastMessage;
public class ScreenCastPlugin extends Plugin {
private ScreenCastPanel screenCastPanel;
private PluginWorkspace workspace;
private ConnectionManager connectionManager;
@Override
public String getName() {
return "ScreenCast";
}
@Override
public String getDisplayName() {
return "Compartilhar tela";
}
@Override
public String getAuthor() {
return "Flávio Schiavoni";
}
@Override
public String getVersion() {
return "0.1";
}
@Override
public String getIdentifier() {
return "Pitiupi.ScreenCast.Example";
}
@Override
public void createPlugin(ConnectionManager connectionManager, PluginWorkspace workspace) {
this.connectionManager = connectionManager;
this.workspace = workspace;
this.screenCastPanel = new ScreenCastPanel(connectionManager);
bindShellWorkspaceContent(screenCastPanel);
installPeerActions(workspace.peerActions());
}
@Override
public void createPlugin(ConnectionManager connectionManager, GuiManager guiManager) {
this.screenCastPanel = new ScreenCastPanel(connectionManager);
guiManager.createMenu(MenuCategory.Streaming, new ScreenCastMenu(screenCastPanel));
}
@Override
public void onShellTabClosed() {
if (screenCastPanel != null) {
screenCastPanel.stopSharing();
}
}
@Override
public void installPeerActions(PeerActionsRegistrar registrar) {
registrar.register(PeerAction.of(
"screencast.multicast",
"Compartilhar na rede (multicast)",
PeerActionTarget.ALL_PEERS,
target -> {
workspace.openOrFocusTab(getIdentifier(), getDisplayName());
screenCastPanel.focusMulticastSharing();
}
));
registrar.register(PeerAction.of(
"screencast.peer",
"Compartilhar com este peer",
PeerActionTarget.SINGLE_PEER,
target -> {
workspace.openOrFocusTab(getIdentifier(), getDisplayName());
if (target instanceof PeerTarget.Single single) {
screenCastPanel.focusPeerSharing(single.peer());
}
}
));
}
@Override
public List<Class<? extends Message>> getMessages() {
return List.of(ScreenCastMessage.class);
}
@Override
public void receiveMessage(Message message) {
if (message instanceof ScreenCastMessage msg && screenCastPanel != null) {
if (isOwnOutboundFrame(msg)) {
return;
}
ScreenCastReceiveScope scope = classifyIncoming(msg);
recordScreenCastActivity(msg, scope);
screenCastPanel.receiveScreenCastMessage(msg, scope);
}
}
private boolean isOwnOutboundFrame(ScreenCastMessage msg) {
if (connectionManager == null || msg.getName() == null) {
return false;
}
String localName = connectionManager.getUserName();
return localName != null && localName.equals(msg.getName().trim());
}
private ScreenCastReceiveScope classifyIncoming(ScreenCastMessage msg) {
if (connectionManager == null || msg.getName() == null) {
return ScreenCastReceiveScope.BROADCAST;
}
for (UserPeerView view : connectionManager.getPresenceService().listPeerViews()) {
if (!view.isBroadcastTarget()
&& msg.getName().equals(view.getDisplayName())
&& view.getInstanceId() != null) {
return ScreenCastReceiveScope.DIRECT;
}
}
return ScreenCastReceiveScope.BROADCAST;
}
private void recordScreenCastActivity(ScreenCastMessage msg, ScreenCastReceiveScope scope) {
if (connectionManager == null) {
return;
}
PeerActivityInbox inbox = connectionManager.getPeerActivityInbox();
if (scope == ScreenCastReceiveScope.DIRECT) {
for (UserPeerView view : connectionManager.getPresenceService().listPeerViews()) {
if (!view.isBroadcastTarget()
&& msg.getName() != null
&& msg.getName().equals(view.getDisplayName())
&& view.getInstanceId() != null) {
inbox.record(view.getInstanceId(), getIdentifier(), "screencast.peer");
return;
}
}
}
inbox.recordBroadcast(getIdentifier(), "screencast.multicast");
}
}