diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..c5f3f6b
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "java.configuration.updateBuildConfiguration": "interactive"
+}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index cfeca41..e46b5d5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -125,7 +125,7 @@
org.apache.pdfbox
pdfbox
- 2.0.27
+ 2.0.27
@@ -135,6 +135,41 @@
pom
+
+ com.github.sarxos
+ webcam-capture
+ 0.3.12
+
+
+ org.bytedeco
+ javacv
+ 1.5.10
+
+
+ org.bytedeco
+ javacpp
+ 1.5.10
+
+
+ org.bytedeco
+ ffmpeg
+ 6.1.1-1.5.10
+
+
+ org.bytedeco
+ ffmpeg-platform
+ 6.1.1-1.5.10
+
+
+ org.bytedeco
+ opencv
+ 4.9.0-1.5.10
+
+
+ org.bytedeco
+ opencv-platform
+ 4.9.0-1.5.10
+
diff --git a/src/main/java/BsK/client/ui/component/CheckUpPage/CheckUpPage.java b/src/main/java/BsK/client/ui/component/CheckUpPage/CheckUpPage.java
index e2f1289..4062b89 100644
--- a/src/main/java/BsK/client/ui/component/CheckUpPage/CheckUpPage.java
+++ b/src/main/java/BsK/client/ui/component/CheckUpPage/CheckUpPage.java
@@ -43,6 +43,32 @@ import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Properties;
+import java.awt.image.BufferedImage;
+
+// Imports for new functionality
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import javax.imageio.ImageIO;
+
+// Webcam imports
+import com.github.sarxos.webcam.Webcam;
+import com.github.sarxos.webcam.WebcamDiscoveryEvent;
+import com.github.sarxos.webcam.WebcamDiscoveryListener;
+import com.github.sarxos.webcam.WebcamEvent;
+import com.github.sarxos.webcam.WebcamListener;
+import com.github.sarxos.webcam.WebcamPanel;
+import com.github.sarxos.webcam.WebcamResolution;
+
+// JavaCV imports for video recording
+import org.bytedeco.javacv.FFmpegFrameRecorder;
+import org.bytedeco.javacv.Frame;
+import org.bytedeco.javacv.Java2DFrameConverter;
+import org.bytedeco.javacv.OpenCVFrameConverter;
+import org.bytedeco.opencv.opencv_core.IplImage;
+import static org.bytedeco.opencv.global.opencv_core.cvFlip;
@Slf4j
public class CheckUpPage extends JPanel {
@@ -88,6 +114,35 @@ public class CheckUpPage extends JPanel {
private JLabel overallTotalCostLabel;
private static final DecimalFormat df = new DecimalFormat("#,##0");
+ // New member variables for Supersonic View
+ private JPanel supersonicViewPanelGlobal;
+ private JPanel imageGalleryPanel; // Displays thumbnails
+ private JScrollPane imageGalleryScrollPane;
+ private JLabel webcamFeedLabel; // Placeholder for webcam feed
+ private JComboBox webcamDeviceComboBox;
+ private JButton takePictureButton;
+ private JButton recordVideoButton;
+ private String currentCheckupIdForMedia;
+ private Path currentCheckupMediaPath;
+ private javax.swing.Timer imageRefreshTimer;
+ private static final String CHECKUP_MEDIA_BASE_DIR = "src/main/resources/image/checkup_media"; // Updated media directory path
+ private static final int THUMBNAIL_WIDTH = 100;
+ private static final int THUMBNAIL_HEIGHT = 100;
+
+ // Webcam-specific variables
+ private Webcam selectedWebcam;
+ private WebcamPanel webcamPanel;
+ private volatile boolean isRecording = false;
+ private Thread recordingThread;
+ private final Object webcamLock = new Object();
+ private JLabel recordingTimeLabel;
+ private long recordingStartTime;
+ private javax.swing.Timer recordingTimer;
+
+ // Video recording components
+ private FFmpegFrameRecorder recorder;
+ private Java2DFrameConverter frameConverter;
+
boolean returnCell = false;
public void updateQueue() {
NetworkUtil.sendPacket(ClientHandler.ctx.channel(), new GetCheckUpQueueRequest());
@@ -144,6 +199,18 @@ public class CheckUpPage extends JPanel {
public CheckUpPage(MainFrame mainFrame) {
setLayout(new BorderLayout());
+ // Ensure the base media directory exists
+ try {
+ Files.createDirectories(Paths.get(CHECKUP_MEDIA_BASE_DIR));
+ log.info("Created or verified media directory at: {}", CHECKUP_MEDIA_BASE_DIR);
+ } catch (IOException e) {
+ log.error("Failed to create media directory: {}", e.getMessage(), e);
+ JOptionPane.showMessageDialog(this,
+ "Không thể tạo thư mục lưu trữ media: " + e.getMessage(),
+ "Lỗi Khởi Tạo",
+ JOptionPane.ERROR_MESSAGE);
+ }
+
ClientHandler.addResponseListener(GetCheckUpQueueResponse.class, checkUpQueueListener);
ClientHandler.addResponseListener(GetCheckUpQueueUpdateResponse.class, checkUpQueueUpdateListener);
ClientHandler.addResponseListener(GetCustomerHistoryResponse.class, customerHistoryListener);
@@ -385,6 +452,24 @@ public class CheckUpPage extends JPanel {
tableScroll1.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
leftPanel.add(tableScroll1, BorderLayout.CENTER);
+ // New structure for leftPanel to accommodate Supersonic View
+ leftPanel.setLayout(new BorderLayout());
+
+ JPanel queueSectionPanel = new JPanel(new BorderLayout());
+ queueSectionPanel.setOpaque(false);
+ queueSectionPanel.add(topPanel, BorderLayout.NORTH);
+ queueSectionPanel.add(tableScroll1, BorderLayout.CENTER);
+
+ supersonicViewPanelGlobal = createSupersonicViewPanel();
+
+ JSplitPane verticalSplitLeft = new JSplitPane(JSplitPane.VERTICAL_SPLIT, queueSectionPanel, supersonicViewPanelGlobal);
+ verticalSplitLeft.setResizeWeight(0.65); // Queue gets 65% of space initially
+ verticalSplitLeft.setDividerSize(8);
+ verticalSplitLeft.setBorder(BorderFactory.createEmptyBorder());
+ verticalSplitLeft.setContinuousLayout(true);
+
+ leftPanel.add(verticalSplitLeft, BorderLayout.CENTER);
+
// Right Bottom Panel (Details and Actions) - Setup remains similar
rightBottomPanel.setLayout(new BorderLayout());
rightBottomPanel.setBorder(BorderFactory.createCompoundBorder(
@@ -678,9 +763,10 @@ public class CheckUpPage extends JPanel {
splitPaneRight.setDividerSize(5);
JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, splitPaneRight);
- splitPane.setResizeWeight(0.4);
- splitPane.setDividerSize(5);
+ splitPane.setResizeWeight(0.35); // Adjust overall horizontal split
+ splitPane.setDividerSize(8); // Main divider a bit thicker
splitPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
+ splitPane.setContinuousLayout(true);
add(navBar, BorderLayout.NORTH);
add(splitPane, BorderLayout.CENTER);
@@ -689,7 +775,7 @@ public class CheckUpPage extends JPanel {
table1.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
- if (e.getClickCount() == 2) {
+ if (e.getClickCount() == 1) {
SwingUtilities.invokeLater(() -> {
int selectedRow = table1.getSelectedRow();
if (selectedRow != -1) {
@@ -781,6 +867,34 @@ public class CheckUpPage extends JPanel {
}
Patient selectedPatient = patientQueue.get(selectedRowInQueue);
+ // == Supersonic View Reset & Setup ==
+ if (imageRefreshTimer != null && imageRefreshTimer.isRunning()) {
+ imageRefreshTimer.stop();
+ }
+
+ // Clean up webcam resources before switching patients
+ cleanupWebcam();
+
+ currentCheckupIdForMedia = null;
+ currentCheckupMediaPath = null;
+ if (imageGalleryPanel != null) { // Clear previous images
+ imageGalleryPanel.removeAll();
+ JLabel loadingMsg = new JLabel("Đang tải hình ảnh (nếu có)...");
+ loadingMsg.setFont(new Font("Arial", Font.ITALIC, 12));
+ imageGalleryPanel.add(loadingMsg);
+ imageGalleryPanel.revalidate();
+ imageGalleryPanel.repaint();
+ }
+
+ // Enable webcam controls for the new patient
+ if (takePictureButton != null) takePictureButton.setEnabled(false);
+ if (recordVideoButton != null) recordVideoButton.setEnabled(false);
+ if (webcamDeviceComboBox != null) webcamDeviceComboBox.setEnabled(false);
+ if (webcamPanel != null) {
+ webcamPanel.start(); // Start webcam preview for new patient
+ }
+ // == End Supersonic View Reset ==
+
medicinePrescription = new String[0][0];
servicePrescription = new String[0][0];
medDialog = null;
@@ -791,8 +905,14 @@ public class CheckUpPage extends JPanel {
SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy");
try {
Date parsedDate; String dateStr = selectedPatient.getCheckupDate();
- if (dateStr.matches("\\d+")) parsedDate = new Date(Long.parseLong(dateStr));
- else parsedDate = dateFormat.parse(dateStr);
+ if (dateStr != null && dateStr.matches("\\d+")) { // Check if string is all digits (timestamp)
+ parsedDate = new Date(Long.parseLong(dateStr));
+ } else if (dateStr != null && !dateStr.trim().isEmpty()) { // Attempt to parse as dd/MM/yyyy
+ parsedDate = dateFormat.parse(dateStr);
+ } else {
+ log.error("Invalid date format for checkup date: {}", selectedPatient.getCheckupDate());
+ return;
+ }
Calendar calendar = Calendar.getInstance(); calendar.setTime(parsedDate);
datePicker.getModel().setDate(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH));
datePicker.getModel().setSelected(true);
@@ -803,14 +923,20 @@ public class CheckUpPage extends JPanel {
try {
Date parsedDate; String dobStr = selectedPatient.getCustomerDob();
- if (dobStr.matches("\\d+")) parsedDate = new Date(Long.parseLong(dobStr));
- else parsedDate = new SimpleDateFormat("dd/MM/yyyy").parse(dobStr);
+ if (dobStr != null && dobStr.matches("\\d+")) { // Timestamp
+ parsedDate = new Date(Long.parseLong(dobStr));
+ } else if (dobStr != null && !dobStr.trim().isEmpty()){ // Date string
+ parsedDate = new SimpleDateFormat("dd/MM/yyyy").parse(dobStr);
+ } else {
+ log.error("Invalid date format for DOB: {}", selectedPatient.getCustomerDob());
+ return;
+ }
Calendar calendar = Calendar.getInstance(); calendar.setTime(parsedDate);
dobPicker.getModel().setDate(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH));
dobPicker.getModel().setSelected(true);
} catch (ParseException | NumberFormatException exception) {
log.error("Invalid date format for DOB: {}", selectedPatient.getCustomerDob(), exception);
- JOptionPane.showMessageDialog(null, "Định dạng ngày không hợp lệ: " + selectedPatient.getCustomerDob(), "Lỗi định dạng ngày", JOptionPane.ERROR_MESSAGE);
+ JOptionPane.showMessageDialog(null, "Định dạng ngày không hợp lệ cho ngày sinh: " + selectedPatient.getCustomerDob(), "Lỗi định dạng ngày", JOptionPane.ERROR_MESSAGE);
}
customerLastNameField.setText(selectedPatient.getCustomerLastName());
@@ -831,8 +957,8 @@ public class CheckUpPage extends JPanel {
String fullAddress = selectedPatient.getCustomerAddress();
String[] addressParts = fullAddress.split(", ");
- if (addressParts.length >= 1) customerAddressField.setText(addressParts[0]);
- else customerAddressField.setText(fullAddress);
+ if (addressParts.length > 0) customerAddressField.setText(addressParts[0]);
+ else if (fullAddress != null) customerAddressField.setText(fullAddress);
if (addressParts.length == 4) {
String ward = addressParts[1]; String district = addressParts[2]; String province = addressParts[3];
@@ -869,7 +995,47 @@ public class CheckUpPage extends JPanel {
districtModel.removeAllElements(); districtModel.addElement("Huyện"); districtComboBox.setEnabled(false);
wardModel.removeAllElements(); wardModel.addElement("Phường"); wardComboBox.setEnabled(false);
}
- saved = true;
+
+ // Setup for Supersonic View media for the NEWLY selected patient
+ currentCheckupIdForMedia = selectedPatient.getCheckupId();
+ if (currentCheckupIdForMedia != null && !currentCheckupIdForMedia.trim().isEmpty()) {
+ ensureMediaDirectoryExists(currentCheckupIdForMedia);
+ currentCheckupMediaPath = Paths.get(CHECKUP_MEDIA_BASE_DIR, currentCheckupIdForMedia.trim());
+
+ if (Files.exists(currentCheckupMediaPath)) {
+ loadAndDisplayImages(currentCheckupMediaPath); // Load initial images for this patient
+
+ if (imageRefreshTimer != null && !imageRefreshTimer.isRunning()) {
+ imageRefreshTimer.start(); // Start/restart timer for this patient's media
+ }
+ if (takePictureButton != null) takePictureButton.setEnabled(true);
+ if (recordVideoButton != null) recordVideoButton.setEnabled(true);
+ if (webcamDeviceComboBox != null) webcamDeviceComboBox.setEnabled(true);
+ } else {
+ log.error("Media directory does not exist after creation attempt: {}", currentCheckupMediaPath);
+ JOptionPane.showMessageDialog(this,
+ "Không thể truy cập thư mục media cho bệnh nhân này.",
+ "Lỗi Thư Mục",
+ JOptionPane.ERROR_MESSAGE);
+ currentCheckupMediaPath = null;
+ if (takePictureButton != null) takePictureButton.setEnabled(false);
+ if (recordVideoButton != null) recordVideoButton.setEnabled(false);
+ if (webcamDeviceComboBox != null) webcamDeviceComboBox.setEnabled(false);
+ }
+ } else {
+ log.warn("No Checkup ID available (or it is empty) for media operations for selected patient (Customer ID: {}). Disabling media features.", selectedPatient.getCustomerId());
+ if (takePictureButton != null) takePictureButton.setEnabled(false);
+ if (recordVideoButton != null) recordVideoButton.setEnabled(false);
+ if (webcamDeviceComboBox != null) webcamDeviceComboBox.setEnabled(false);
+ if(imageGalleryPanel != null) {
+ imageGalleryPanel.removeAll();
+ imageGalleryPanel.add(new JLabel("Không có ID khám để hiển thị media."));
+ imageGalleryPanel.revalidate();
+ imageGalleryPanel.repaint();
+ }
+ }
+
+ saved = false; // Data loaded, any change from now on is "unsaved" until save button.
}
private void handleGetDistrictResponse(GetDistrictResponse response) {
@@ -903,7 +1069,7 @@ public class CheckUpPage extends JPanel {
}
private void handleGetDoctorGeneralInfoResponse(GetDoctorGeneralInfoResponse response) {
- log.info("Received doctor general info (though LocalStorage.doctorsName is typically used)");
+ log.info("Received doctor general info (though LocalStorage.doctorsName is typically used directly)");
}
private void handleGetCheckUpQueueUpdateResponse(GetCheckUpQueueUpdateResponse response) {
@@ -948,14 +1114,16 @@ public class CheckUpPage extends JPanel {
if (patient != null && patientIdToFind.equals(patient.getCheckupId())) {
String ho = patient.getCustomerLastName(); String ten = patient.getCustomerFirstName();
String customerDob = patient.getCustomerDob(); String namSinh = "N/A";
- try {
- Date parsedDate;
- if (customerDob.matches("\\d+")) parsedDate = new Date(Long.parseLong(customerDob));
- else parsedDate = new SimpleDateFormat("dd/MM/yyyy").parse(customerDob);
- Calendar calendar = Calendar.getInstance(); calendar.setTime(parsedDate);
- namSinh = String.valueOf(calendar.get(Calendar.YEAR));
- } catch (ParseException | NumberFormatException e) {
- log.error("Error parsing DoB for TV display: {} for patient ID {}", customerDob, patientIdToFind, e);
+ if (customerDob != null && !customerDob.isEmpty()) {
+ try {
+ Date parsedDate;
+ if (customerDob.matches("\\d+")) parsedDate = new Date(Long.parseLong(customerDob)); // Timestamp
+ else parsedDate = new SimpleDateFormat("dd/MM/yyyy").parse(customerDob); // Date string
+ Calendar calendar = Calendar.getInstance(); calendar.setTime(parsedDate);
+ namSinh = String.valueOf(calendar.get(Calendar.YEAR));
+ } catch (ParseException | NumberFormatException e) {
+ log.error("Error parsing DoB for TV display: {} for patient ID {}", customerDob, patientIdToFind, e);
+ }
}
patientDisplayInfo = ho + " " + ten + " (" + namSinh + ")";
break;
@@ -1013,5 +1181,617 @@ public class CheckUpPage extends JPanel {
callingStatusLabel.setForeground(Color.WHITE);
callingStatusLabel.setBackground(new Color(217, 83, 79)); // Red background for calling
}
+
+ private JPanel createSupersonicViewPanel() {
+ JPanel panel = new JPanel(new BorderLayout(5, 5));
+ panel.setBorder(BorderFactory.createCompoundBorder(
+ BorderFactory.createEmptyBorder(5, 0, 0, 0), // Top margin for this section
+ BorderFactory.createTitledBorder(
+ BorderFactory.createLineBorder(new Color(63, 81, 181), 1, true),
+ "Siêu âm & Hình ảnh",
+ TitledBorder.LEADING, TitledBorder.TOP,
+ new Font("Arial", Font.BOLD, 16), new Color(63, 81, 181))));
+ panel.setMinimumSize(new Dimension(300, 250)); // Ensure it has some minimum height
+ panel.setPreferredSize(new Dimension(450, 300)); // Give it a decent preferred size
+
+ JPanel imageDisplayContainer = createImageDisplayPanel(); // Left side: Image gallery
+ JPanel webcamControlContainer = createWebcamControlPanel(); // Right side: Webcam controls
+
+ JSplitPane supersonicSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, imageDisplayContainer, webcamControlContainer);
+ supersonicSplitPane.setResizeWeight(0.60); // Image gallery gets 60% of the space
+ supersonicSplitPane.setDividerSize(6);
+ supersonicSplitPane.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); // Inner padding
+ supersonicSplitPane.setContinuousLayout(true);
+
+ panel.add(supersonicSplitPane, BorderLayout.CENTER);
+
+ // Initialize image refresh timer (if not already)
+ if (imageRefreshTimer == null) {
+ imageRefreshTimer = new javax.swing.Timer(5000, e -> { // Refresh every 5 seconds
+ if (currentCheckupMediaPath != null && Files.exists(currentCheckupMediaPath)) {
+ loadAndDisplayImages(currentCheckupMediaPath);
+ }
+ });
+ imageRefreshTimer.setRepeats(true);
+ }
+ return panel;
+ }
+
+ private JPanel createImageDisplayPanel() {
+ JPanel panel = new JPanel(new BorderLayout(5, 5));
+ panel.setBorder(BorderFactory.createTitledBorder(
+ BorderFactory.createEtchedBorder(), "Thư viện Hình ảnh",
+ TitledBorder.CENTER, TitledBorder.TOP,
+ new Font("Arial", Font.ITALIC, 14), new Color(50, 50, 50)));
+
+ imageGalleryPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 10)); // Added more spacing
+ imageGalleryPanel.setBackground(Color.WHITE); // Set a background for the gallery
+
+ imageGalleryScrollPane = new JScrollPane(imageGalleryPanel);
+ imageGalleryScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
+ imageGalleryScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
+ imageGalleryScrollPane.getVerticalScrollBar().setUnitIncrement(16);
+ imageGalleryScrollPane.setBorder(BorderFactory.createLineBorder(Color.LIGHT_GRAY)); // Border for scroll pane
+
+ panel.add(imageGalleryScrollPane, BorderLayout.CENTER);
+ panel.setPreferredSize(new Dimension(250, 0)); // Width preference, height will be determined by split
+ return panel;
+ }
+
+ private JPanel createWebcamControlPanel() {
+ JPanel panel = new JPanel(new BorderLayout(5, 10));
+ panel.setBorder(BorderFactory.createTitledBorder(
+ BorderFactory.createEtchedBorder(), "Điều khiển Webcam",
+ TitledBorder.CENTER, TitledBorder.TOP,
+ new Font("Arial", Font.ITALIC, 14), new Color(50, 50, 50)));
+ panel.setBackground(Color.WHITE);
+
+ // Initialize webcam components - only scan for devices once
+ Webcam.getDiscoveryService().stop(); // Stop automatic discovery
+
+ List webcams = Webcam.getWebcams();
+ String[] webcamNames = webcams.stream()
+ .map(Webcam::getName)
+ .toArray(String[]::new);
+
+ // Panel for device selection
+ JPanel devicePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ devicePanel.setOpaque(false);
+ devicePanel.add(new JLabel("Thiết bị:"));
+ webcamDeviceComboBox = new JComboBox<>(webcamNames.length > 0 ? webcamNames : new String[]{"Không tìm thấy webcam"});
+ webcamDeviceComboBox.addActionListener(e -> {
+ String selectedDevice = (String) webcamDeviceComboBox.getSelectedItem();
+ if (selectedDevice != null && !selectedDevice.equals("Không tìm thấy webcam")) {
+ switchWebcam(selectedDevice);
+ }
+ });
+ devicePanel.add(webcamDeviceComboBox);
+
+ // Create webcam panel with default webcam
+ if (!webcams.isEmpty()) {
+ selectedWebcam = webcams.get(0);
+ // Try to set a higher custom resolution
+ Dimension[] resolutions = selectedWebcam.getViewSizes();
+ Dimension bestResolution = WebcamResolution.VGA.getSize();
+
+ // Find the best resolution that's at least VGA but not too high
+ for (Dimension resolution : resolutions) {
+ if (resolution.width >= 640 && resolution.height >= 480 &&
+ resolution.width <= 1280 && resolution.height <= 720) {
+ bestResolution = resolution;
+ break;
+ }
+ }
+
+ selectedWebcam.setViewSize(bestResolution);
+ webcamPanel = new WebcamPanel(selectedWebcam, false); // false = don't start automatically
+ webcamPanel.setFPSDisplayed(true);
+ webcamPanel.setPreferredSize(new Dimension(180, 140));
+ webcamPanel.setFitArea(true);
+ webcamPanel.setFPSLimit(30); // Limit FPS to reduce unnecessary updates
+ webcamPanel.start();
+ } else {
+ webcamPanel = null;
+ JLabel noWebcamLabel = new JLabel("Không tìm thấy webcam", SwingConstants.CENTER);
+ noWebcamLabel.setPreferredSize(new Dimension(180, 140));
+ panel.add(noWebcamLabel, BorderLayout.CENTER);
+ }
+
+ if (webcamPanel != null) {
+ panel.add(webcamPanel, BorderLayout.CENTER);
+ }
+
+ // Recording time label
+ recordingTimeLabel = new JLabel("00:00:00", SwingConstants.CENTER);
+ recordingTimeLabel.setFont(new Font("Monospaced", Font.BOLD, 14));
+ recordingTimeLabel.setForeground(Color.RED);
+ recordingTimeLabel.setVisible(false);
+
+ // Initialize recording timer
+ recordingTimer = new javax.swing.Timer(1000, e -> updateRecordingTime());
+
+ // Panel for buttons
+ JPanel buttonPanel = new JPanel(new GridLayout(1, 2, 5, 5)); // Changed to 1 row, 2 columns
+ buttonPanel.setOpaque(false);
+
+ takePictureButton = new JButton("Chụp ảnh");
+ takePictureButton.setIcon(new ImageIcon("src/main/java/BsK/client/ui/assets/icon/camera.png"));
+ takePictureButton.addActionListener(e -> handleTakePicture());
+
+ recordVideoButton = new JButton("Quay video");
+ recordVideoButton.setIcon(new ImageIcon("src/main/java/BsK/client/ui/assets/icon/video-camera.png"));
+ recordVideoButton.addActionListener(e -> handleRecordVideo());
+
+ buttonPanel.add(takePictureButton);
+ buttonPanel.add(recordVideoButton);
+
+ // Layout for webcam controls
+ JPanel southControls = new JPanel(new BorderLayout(5,5));
+ southControls.setOpaque(false);
+ southControls.add(devicePanel, BorderLayout.NORTH);
+ southControls.add(recordingTimeLabel, BorderLayout.CENTER);
+ southControls.add(buttonPanel, BorderLayout.SOUTH);
+ panel.add(southControls, BorderLayout.SOUTH);
+
+ // Initially disable buttons until a patient is selected
+ takePictureButton.setEnabled(false);
+ recordVideoButton.setEnabled(false);
+ webcamDeviceComboBox.setEnabled(false);
+
+ panel.setPreferredSize(new Dimension(200,0));
+ return panel;
+ }
+
+ private void updateRecordingTime() {
+ if (!isRecording) return;
+
+ long elapsedTime = System.currentTimeMillis() - recordingStartTime;
+ long seconds = (elapsedTime / 1000) % 60;
+ long minutes = (elapsedTime / (1000 * 60)) % 60;
+ long hours = (elapsedTime / (1000 * 60 * 60)) % 24;
+
+ recordingTimeLabel.setText(String.format("%02d:%02d:%02d", hours, minutes, seconds));
+ }
+
+ private void switchWebcam(String deviceName) {
+ synchronized (webcamLock) {
+ if (selectedWebcam != null && selectedWebcam.isOpen()) {
+ webcamPanel.stop();
+ selectedWebcam.close();
+ }
+
+ // Find webcam from the existing list without triggering new discovery
+ Webcam newWebcam = Webcam.getWebcams().stream()
+ .filter(webcam -> webcam.getName().equals(deviceName))
+ .findFirst()
+ .orElse(null);
+
+ if (newWebcam != null) {
+ selectedWebcam = newWebcam;
+ // Try to set a higher custom resolution
+ Dimension[] resolutions = selectedWebcam.getViewSizes();
+ Dimension bestResolution = WebcamResolution.VGA.getSize();
+
+ // Find the best resolution that's at least VGA but not too high
+ for (Dimension resolution : resolutions) {
+ if (resolution.width >= 640 && resolution.height >= 480 &&
+ resolution.width <= 1280 && resolution.height <= 720) {
+ bestResolution = resolution;
+ break;
+ }
+ }
+
+ selectedWebcam.setViewSize(bestResolution);
+
+ // Remove old webcam panel
+ if (webcamPanel != null && webcamPanel.getParent() != null) {
+ Container parent = webcamPanel.getParent();
+ parent.remove(webcamPanel);
+
+ // Create and add new webcam panel
+ webcamPanel = new WebcamPanel(selectedWebcam, false); // false = don't start automatically
+ webcamPanel.setFPSDisplayed(true);
+ webcamPanel.setPreferredSize(new Dimension(180, 140));
+ webcamPanel.setFitArea(true);
+ parent.add(webcamPanel, BorderLayout.CENTER);
+ parent.revalidate();
+ parent.repaint();
+ webcamPanel.start();
+ }
+ }
+ }
+ }
+
+ private void handleTakePicture() {
+ if (currentCheckupMediaPath == null) {
+ JOptionPane.showMessageDialog(this, "Vui lòng chọn một lượt khám để lưu ảnh.", "Chưa chọn lượt khám", JOptionPane.WARNING_MESSAGE);
+ return;
+ }
+
+ synchronized (webcamLock) {
+ if (selectedWebcam == null || !selectedWebcam.isOpen()) {
+ JOptionPane.showMessageDialog(this, "Webcam không khả dụng.", "Lỗi Webcam", JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+
+ String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
+ String fileName = "IMG_" + currentCheckupIdForMedia + "_" + timestamp + ".png";
+ Path filePath = currentCheckupMediaPath.resolve(fileName);
+
+ try {
+ BufferedImage image = selectedWebcam.getImage();
+ if (image != null) {
+ ImageIO.write(image, "PNG", filePath.toFile());
+ log.info("Picture taken and saved at: {}", filePath);
+ JOptionPane.showMessageDialog(this,
+ "Đã chụp và lưu ảnh thành công tại:\n" + filePath.toString(),
+ "Chụp ảnh thành công", JOptionPane.INFORMATION_MESSAGE);
+ loadAndDisplayImages(currentCheckupMediaPath);
+ } else {
+ throw new IOException("Không thể lấy ảnh từ webcam");
+ }
+ } catch (IOException ex) {
+ log.error("Error capturing/saving image: {}", filePath, ex);
+ JOptionPane.showMessageDialog(this,
+ "Lỗi khi chụp/lưu ảnh: " + ex.getMessage(),
+ "Lỗi Chụp Ảnh",
+ JOptionPane.ERROR_MESSAGE);
+ }
+ }
+ }
+
+ private void handleRecordVideo() {
+ if (currentCheckupMediaPath == null) {
+ JOptionPane.showMessageDialog(this, "Vui lòng chọn một lượt khám để lưu video.", "Chưa chọn lượt khám", JOptionPane.WARNING_MESSAGE);
+ return;
+ }
+
+ synchronized (webcamLock) {
+ if (selectedWebcam == null || !selectedWebcam.isOpen()) {
+ JOptionPane.showMessageDialog(this, "Webcam không khả dụng.", "Lỗi Webcam", JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+
+ if (!isRecording) {
+ // Start recording
+ String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
+ String fileName = "VID_" + currentCheckupIdForMedia + "_" + timestamp + ".mp4";
+ Path filePath = currentCheckupMediaPath.resolve(fileName);
+
+ try {
+ // Check if JavaCV classes are available
+ try {
+ Class.forName("org.bytedeco.javacv.Java2DFrameConverter");
+ Class.forName("org.bytedeco.javacv.FFmpegFrameRecorder");
+ } catch (ClassNotFoundException e) {
+ throw new Exception("Không tìm thấy thư viện JavaCV cần thiết. Vui lòng kiểm tra cài đặt Maven.", e);
+ }
+
+ // Initialize frame converter
+ frameConverter = new Java2DFrameConverter();
+
+ // Get webcam's actual frame rate
+ double webcamFPS = selectedWebcam.getFPS();
+ // If webcam doesn't report FPS or reports an invalid value, default to 15
+ if (webcamFPS <= 0 || Double.isNaN(webcamFPS)) {
+ webcamFPS = 15.0;
+ }
+
+ // Set output FPS to match capture rate but use PTS multiplier for slower playback
+ final double captureRate = webcamFPS;
+
+ // Initialize the recorder with optimized settings
+ try {
+ Dimension size = selectedWebcam.getViewSize();
+ recorder = new FFmpegFrameRecorder(filePath.toString(), size.width, size.height);
+
+ // Video format and codec settings
+ recorder.setVideoCodec(org.bytedeco.ffmpeg.global.avcodec.AV_CODEC_ID_H264);
+ recorder.setFormat("mp4");
+ recorder.setFrameRate(captureRate);
+ recorder.setVideoQuality(1);
+
+ // Pixel format and color space settings
+ recorder.setPixelFormat(org.bytedeco.ffmpeg.global.avutil.AV_PIX_FMT_YUV420P);
+
+ // Encoding settings for better performance
+ recorder.setVideoOption("preset", "medium");
+ recorder.setVideoOption("tune", "zerolatency");
+ recorder.setVideoOption("crf", "23");
+
+ // Buffer size settings - increased for smoother recording
+ recorder.setVideoOption("bufsize", "5000k");
+ recorder.setVideoOption("maxrate", "5000k");
+
+ // Add filter to slow down playback by 3x
+ recorder.setVideoOption("vf", "setpts=3.0*PTS");
+
+ recorder.start();
+ } catch (Exception e) {
+ throw new Exception("Lỗi khởi tạo FFmpeg recorder: " + e.getMessage(), e);
+ }
+
+ recordingThread = new Thread(() -> {
+ try {
+ recordingStartTime = System.currentTimeMillis();
+ long frameInterval = (long) (1000.0 / captureRate); // Time between frames in ms
+ long lastFrameTime = System.currentTimeMillis();
+ long frameCount = 0;
+ long startTime = System.currentTimeMillis();
+
+ while (isRecording) {
+ if (selectedWebcam.isOpen()) {
+ long currentTime = System.currentTimeMillis();
+ long elapsedTime = currentTime - lastFrameTime;
+
+ if (elapsedTime >= frameInterval) {
+ BufferedImage image = selectedWebcam.getImage();
+ if (image != null) {
+ try {
+ // Convert color space from BGR to RGB
+ BufferedImage rgbImage = new BufferedImage(
+ image.getWidth(), image.getHeight(),
+ BufferedImage.TYPE_3BYTE_BGR);
+
+ Graphics2D g = rgbImage.createGraphics();
+ g.drawImage(image, 0, 0, null);
+ g.dispose();
+
+ Frame frame = frameConverter.convert(rgbImage);
+ recorder.record(frame);
+
+ frameCount++;
+ lastFrameTime = currentTime;
+
+ // Calculate actual FPS every 30 frames
+ if (frameCount % 30 == 0) {
+ long duration = System.currentTimeMillis() - startTime;
+ double actualFPS = (frameCount * 1000.0) / duration;
+ log.debug("Actual recording FPS: {}", String.format("%.2f", actualFPS));
+ }
+ } catch (Exception e) {
+ throw new Exception("Lỗi ghi frame: " + e.getMessage(), e);
+ }
+ }
+ } else {
+ // Sleep for a shorter time to be more responsive
+ Thread.sleep(Math.max(1, frameInterval - elapsedTime));
+ }
+ } else {
+ Thread.sleep(100);
+ }
+ }
+ } catch (Exception e) {
+ log.error("Error during video recording: {}", e.getMessage(), e);
+ SwingUtilities.invokeLater(() -> {
+ JOptionPane.showMessageDialog(CheckUpPage.this,
+ "Lỗi trong quá trình ghi video: " + e.getMessage(),
+ "Lỗi Ghi Video",
+ JOptionPane.ERROR_MESSAGE);
+ stopRecording();
+ });
+ }
+ }, "VideoRecordingThread");
+
+ recordingThread.setPriority(Thread.MAX_PRIORITY);
+
+ isRecording = true;
+ recordVideoButton.setText("Dừng");
+ recordVideoButton.setBackground(Color.RED);
+ recordingTimeLabel.setVisible(true);
+ recordingTimeLabel.setForeground(Color.RED);
+ recordingTimer.start();
+ recordingThread.start();
+
+ log.info("Started video recording to: {}", filePath);
+ } catch (Exception e) {
+ log.error("Error initializing video recording: {}", e.getMessage(), e);
+ String errorMessage = e.getMessage();
+ if (e.getCause() != null) {
+ errorMessage += "\n\nChi tiết lỗi: " + e.getCause().getMessage();
+ }
+ JOptionPane.showMessageDialog(this,
+ "Lỗi khởi tạo ghi video:\n" + errorMessage,
+ "Lỗi Ghi Video",
+ JOptionPane.ERROR_MESSAGE);
+ stopRecording();
+ }
+ } else {
+ stopRecording();
+ }
+ }
+ }
+
+ private void stopRecording() {
+ isRecording = false;
+
+ if (recordingThread != null) {
+ try {
+ recordingThread.join(1000); // Wait for recording thread to finish
+ } catch (InterruptedException e) {
+ log.warn("Interrupted while waiting for recording thread to finish");
+ }
+ recordingThread = null;
+ }
+
+ if (recorder != null) {
+ try {
+ recorder.stop();
+ recorder.release();
+ } catch (Exception e) {
+ log.error("Error stopping recorder: {}", e.getMessage(), e);
+ }
+ recorder = null;
+ }
+
+ if (frameConverter != null) {
+ frameConverter = null;
+ }
+
+ recordVideoButton.setText("Quay video");
+ recordVideoButton.setBackground(null);
+ recordingTimeLabel.setVisible(false);
+ recordingTimer.stop();
+
+ log.info("Stopped video recording");
+ }
+
+ // Add cleanup method for webcam resources
+ private void cleanupWebcam() {
+ synchronized (webcamLock) {
+ if (webcamPanel != null) {
+ webcamPanel.stop();
+ }
+ if (selectedWebcam != null && selectedWebcam.isOpen()) {
+ selectedWebcam.close();
+ }
+ if (isRecording) {
+ isRecording = false;
+ if (recordingThread != null) {
+ recordingThread.interrupt();
+ recordingThread = null;
+ }
+ }
+ }
+ }
+
+ private void loadAndDisplayImages(Path mediaPath) {
+ if (imageGalleryPanel == null) {
+ log.warn("imageGalleryPanel is null, cannot load images.");
+ return;
+ }
+ // Ensure this runs on the EDT
+ if (!SwingUtilities.isEventDispatchThread()) {
+ SwingUtilities.invokeLater(() -> loadAndDisplayImages(mediaPath));
+ return;
+ }
+
+ imageGalleryPanel.removeAll(); // Clear existing images
+
+ File mediaFolder = mediaPath.toFile();
+ if (mediaFolder.exists() && mediaFolder.isDirectory()) {
+ File[] files = mediaFolder.listFiles((dir, name) -> {
+ String lowerName = name.toLowerCase();
+ return lowerName.endsWith(".png") ||
+ lowerName.endsWith(".jpg") ||
+ lowerName.endsWith(".jpeg") ||
+ lowerName.endsWith(".gif");
+ });
+
+ if (files != null && files.length > 0) {
+ for (File file : files) {
+ try {
+ ImageIcon originalIcon = new ImageIcon(file.toURI().toURL()); // Use URL to avoid caching issues
+ Image image = originalIcon.getImage();
+
+ int originalWidth = originalIcon.getIconWidth();
+ int originalHeight = originalIcon.getIconHeight();
+ int newWidth = THUMBNAIL_WIDTH;
+ int newHeight = THUMBNAIL_HEIGHT;
+
+ if (originalWidth > 0 && originalHeight > 0) {
+ double aspectRatio = (double) originalWidth / originalHeight;
+ if (originalWidth > originalHeight) { // Landscape or square
+ newHeight = (int) (THUMBNAIL_WIDTH / aspectRatio);
+ } else { // Portrait
+ newWidth = (int) (THUMBNAIL_HEIGHT * aspectRatio);
+ }
+ // Ensure new dimensions are at least 1x1
+ newWidth = Math.max(1, newWidth);
+ newHeight = Math.max(1, newHeight);
+ } else {
+ // Fallback for invalid image dimensions
+ log.warn("Image {} has invalid dimensions ({}x{})", file.getName(), originalWidth, originalHeight);
+ newWidth = THUMBNAIL_WIDTH / 2; newHeight = THUMBNAIL_HEIGHT / 2;
+ }
+
+
+ Image scaledImage = image.getScaledInstance(newWidth, newHeight, Image.SCALE_SMOOTH);
+ JLabel imageLabel = new JLabel(new ImageIcon(scaledImage));
+ imageLabel.setToolTipText(file.getName() + " (" + originalWidth + "x" + originalHeight + ")");
+ imageLabel.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY));
+ // Set fixed size for the label to ensure FlowLayout behaves
+ imageLabel.setPreferredSize(new Dimension(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT));
+ imageLabel.setHorizontalAlignment(SwingConstants.CENTER);
+ imageLabel.setVerticalAlignment(SwingConstants.CENTER);
+
+ imageGalleryPanel.add(imageLabel);
+ } catch (Exception ex) {
+ log.error("Error loading image thumbnail: {}", file.getAbsolutePath(), ex);
+ JLabel errorLabel = new JLabel("Lỗi ảnh
" + file.getName().substring(0, Math.min(file.getName().length(),10)) + "...");
+ errorLabel.setPreferredSize(new Dimension(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT));
+ errorLabel.setHorizontalAlignment(SwingConstants.CENTER);
+ errorLabel.setOpaque(true);
+ errorLabel.setForeground(Color.RED);
+ errorLabel.setBackground(Color.LIGHT_GRAY);
+ imageGalleryPanel.add(errorLabel);
+ }
+ }
+ } else {
+ JLabel noImagesLabel = new JLabel("Không có hình ảnh trong thư mục.");
+ noImagesLabel.setFont(new Font("Arial", Font.ITALIC, 12));
+ noImagesLabel.setHorizontalAlignment(SwingConstants.CENTER);
+ // Make label take up some space if gallery is empty
+ noImagesLabel.setPreferredSize(new Dimension(imageGalleryScrollPane.getViewport().getWidth() - 20 > 0 ? imageGalleryScrollPane.getViewport().getWidth() - 20 : 150, THUMBNAIL_HEIGHT));
+ imageGalleryPanel.add(noImagesLabel);
+ }
+ } else {
+ JLabel noFolderLabel = new JLabel("Thư mục media không tồn tại hoặc không thể truy cập.");
+ noFolderLabel.setFont(new Font("Arial", Font.ITALIC, 12));
+ noFolderLabel.setHorizontalAlignment(SwingConstants.CENTER);
+ noFolderLabel.setPreferredSize(new Dimension(imageGalleryScrollPane.getViewport().getWidth() - 20 > 0 ? imageGalleryScrollPane.getViewport().getWidth() - 20 : 200, THUMBNAIL_HEIGHT));
+ imageGalleryPanel.add(noFolderLabel);
+ log.warn("Media path does not exist or is not a directory: {}", mediaPath);
+ }
+ imageGalleryPanel.revalidate();
+ imageGalleryPanel.repaint();
+ }
+
+ public void cleanup() {
+ // Stop image refresh timer
+ if (imageRefreshTimer != null && imageRefreshTimer.isRunning()) {
+ imageRefreshTimer.stop();
+ }
+
+ // Clean up webcam resources
+ cleanupWebcam();
+
+ // Stop webcam discovery service
+ Webcam.getDiscoveryService().stop();
+ }
+
+ @Override
+ public void removeNotify() {
+ stopRecording(); // Ensure recording is stopped when component is removed
+ cleanup();
+ super.removeNotify();
+ }
+
+ private void ensureMediaDirectoryExists(String checkupId) {
+ try {
+ // First ensure base directory exists
+ Path baseDir = Paths.get(CHECKUP_MEDIA_BASE_DIR);
+ if (!Files.exists(baseDir)) {
+ Files.createDirectories(baseDir);
+ log.info("Created base media directory at: {}", baseDir);
+ }
+
+ // Then create patient-specific directory
+ if (checkupId != null && !checkupId.trim().isEmpty()) {
+ Path patientDir = baseDir.resolve(checkupId.trim());
+ if (!Files.exists(patientDir)) {
+ Files.createDirectories(patientDir);
+ log.info("Created patient media directory at: {}", patientDir);
+ }
+ return;
+ }
+ } catch (IOException e) {
+ log.error("Error creating media directories for checkup {}: {}", checkupId, e.getMessage(), e);
+ JOptionPane.showMessageDialog(null,
+ "Không thể tạo thư mục lưu trữ media: " + e.getMessage(),
+ "Lỗi Thư Mục",
+ JOptionPane.ERROR_MESSAGE);
+ }
+ }
}
diff --git a/src/main/resources/image/checkup_media/11/IMG_11_20250604_025917.png b/src/main/resources/image/checkup_media/11/IMG_11_20250604_025917.png
new file mode 100644
index 0000000..4a45334
Binary files /dev/null and b/src/main/resources/image/checkup_media/11/IMG_11_20250604_025917.png differ
diff --git a/src/main/resources/image/checkup_media/11/VID_11_20250604_032332.mp4 b/src/main/resources/image/checkup_media/11/VID_11_20250604_032332.mp4
new file mode 100644
index 0000000..f82fb92
Binary files /dev/null and b/src/main/resources/image/checkup_media/11/VID_11_20250604_032332.mp4 differ
diff --git a/src/main/resources/image/checkup_media/13/VID_13_20250604_032008.mp4 b/src/main/resources/image/checkup_media/13/VID_13_20250604_032008.mp4
new file mode 100644
index 0000000..36df072
Binary files /dev/null and b/src/main/resources/image/checkup_media/13/VID_13_20250604_032008.mp4 differ
diff --git a/src/main/resources/image/checkup_media/3/IMG_3_20250604_031612.png b/src/main/resources/image/checkup_media/3/IMG_3_20250604_031612.png
new file mode 100644
index 0000000..526c0df
Binary files /dev/null and b/src/main/resources/image/checkup_media/3/IMG_3_20250604_031612.png differ
diff --git a/src/main/resources/image/checkup_media/3/VID_3_20250604_031615.mp4 b/src/main/resources/image/checkup_media/3/VID_3_20250604_031615.mp4
new file mode 100644
index 0000000..feb280b
Binary files /dev/null and b/src/main/resources/image/checkup_media/3/VID_3_20250604_031615.mp4 differ
diff --git a/src/main/resources/image/checkup_media/6/1cfa34a2b4125ed44fed8283bd56676b.jpg b/src/main/resources/image/checkup_media/6/1cfa34a2b4125ed44fed8283bd56676b.jpg
new file mode 100644
index 0000000..e46e8a2
Binary files /dev/null and b/src/main/resources/image/checkup_media/6/1cfa34a2b4125ed44fed8283bd56676b.jpg differ
diff --git a/src/main/resources/image/checkup_media/6/IMG_6_20250604_025427.png b/src/main/resources/image/checkup_media/6/IMG_6_20250604_025427.png
new file mode 100644
index 0000000..382d95e
Binary files /dev/null and b/src/main/resources/image/checkup_media/6/IMG_6_20250604_025427.png differ
diff --git a/src/main/resources/image/checkup_media/6/IMG_6_20250604_025430.png b/src/main/resources/image/checkup_media/6/IMG_6_20250604_025430.png
new file mode 100644
index 0000000..d2dab45
Binary files /dev/null and b/src/main/resources/image/checkup_media/6/IMG_6_20250604_025430.png differ
diff --git a/src/main/resources/image/checkup_media/9/VID_9_20250604_030213.mp4 b/src/main/resources/image/checkup_media/9/VID_9_20250604_030213.mp4
new file mode 100644
index 0000000..3096a7c
Binary files /dev/null and b/src/main/resources/image/checkup_media/9/VID_9_20250604_030213.mp4 differ
diff --git a/src/main/resources/image/checkup_media/9/VID_9_20250604_030547.mp4 b/src/main/resources/image/checkup_media/9/VID_9_20250604_030547.mp4
new file mode 100644
index 0000000..9975aa8
Binary files /dev/null and b/src/main/resources/image/checkup_media/9/VID_9_20250604_030547.mp4 differ