[add] Integrate webcam functionality into CheckUpPage, allowing users to capture images and record videos. Update pom.xml with new dependencies for webcam and video processing libraries.

This commit is contained in:
Lê Thành Đạt committed 2025-06-04 03:29:54 +07:00
1 parent b2555b0803
commit 180364696b
13 files changed
+839 -21

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "interactive"
}
+36 -1
View File
@@ -125,7 +125,7 @@
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.27</version> <!-- Use the latest version -->
<version>2.0.27</version>
</dependency>
<dependency>
@@ -135,6 +135,41 @@
<type>pom</type>
</dependency>
<dependency>
<groupId>com.github.sarxos</groupId>
<artifactId>webcam-capture</artifactId>
<version>0.3.12</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>1.5.10</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacpp</artifactId>
<version>1.5.10</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>6.1.1-1.5.10</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg-platform</artifactId>
<version>6.1.1-1.5.10</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>opencv</artifactId>
<version>4.9.0-1.5.10</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>opencv-platform</artifactId>
<version>4.9.0-1.5.10</version>
</dependency>
</dependencies>
</project>
@@ -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<String> 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<Webcam> 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("<html><center>Lỗi ảnh<br>" + file.getName().substring(0, Math.min(file.getName().length(),10)) + "...</center></html>");
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);
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB