From 69e2f3ae1d8b3e0cfb2d7a065d20724230bc0d2d Mon Sep 17 00:00:00 2001 From: Maik Jurischka Date: Thu, 18 Dec 2025 16:10:55 +0100 Subject: [PATCH] first commit --- .gitignore | 53 +++ CMakeLists.txt | 47 +++ README.md | 347 +++++++++++++++++++ SOCKET_API.md | 663 ++++++++++++++++++++++++++++++++++++ build.sh | 28 ++ cameracontrolwidget.cpp | 381 +++++++++++++++++++++ cameracontrolwidget.h | 88 +++++ gstreamerpipelinewidget.cpp | 328 ++++++++++++++++++ gstreamerpipelinewidget.h | 46 +++ main.cpp | 11 + mainwindow.cpp | 49 +++ mainwindow.h | 33 ++ mainwindow.ui | 31 ++ run.sh | 28 ++ socketclient.cpp | 111 ++++++ socketclient.h | 33 ++ test_connection.sh | 52 +++ videoviewerwidget.cpp | 323 ++++++++++++++++++ videoviewerwidget.h | 55 +++ 19 files changed, 2707 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 SOCKET_API.md create mode 100755 build.sh create mode 100644 cameracontrolwidget.cpp create mode 100644 cameracontrolwidget.h create mode 100644 gstreamerpipelinewidget.cpp create mode 100644 gstreamerpipelinewidget.h create mode 100644 main.cpp create mode 100644 mainwindow.cpp create mode 100644 mainwindow.h create mode 100644 mainwindow.ui create mode 100755 run.sh create mode 100644 socketclient.cpp create mode 100644 socketclient.h create mode 100755 test_connection.sh create mode 100644 videoviewerwidget.cpp create mode 100644 videoviewerwidget.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6742ac6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,53 @@ +# CMake build files +build/ +build-*/ +CMakeFiles/ +CMakeCache.txt +cmake_install.cmake +Makefile +*.cmake + +# Qt Creator files +*.pro.user +*.pro.user.* +.qtcreator/ +*.autosave + +# Compiled Object files +*.o +*.obj +*.so +*.dylib +*.dll + +# Executables +gstreamerViewer +*.exe +*.app + +# Qt Meta Object Compiler files +moc_*.cpp +moc_*.h +qrc_*.cpp +ui_*.h + +# Qt Resource Compiler +*.qrc.depends +*.qm + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ +.DS_Store + +# Debug and temporary files +*.log +core +*.core + +# Documentation generation +html/ +latex/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..99fef5d --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,47 @@ +cmake_minimum_required(VERSION 3.19) +project(gstreamerViewer LANGUAGES CXX) + +find_package(Qt6 6.5 REQUIRED COMPONENTS Core Widgets) +find_package(PkgConfig REQUIRED) +pkg_check_modules(GSTREAMER REQUIRED gstreamer-1.0 gstreamer-video-1.0) + +qt_standard_project_setup() + +qt_add_executable(gstreamerViewer + WIN32 MACOSX_BUNDLE + main.cpp + mainwindow.cpp + mainwindow.h + mainwindow.ui + socketclient.cpp + socketclient.h + gstreamerpipelinewidget.cpp + gstreamerpipelinewidget.h + cameracontrolwidget.cpp + cameracontrolwidget.h + videoviewerwidget.cpp + videoviewerwidget.h +) + +target_include_directories(gstreamerViewer PRIVATE ${GSTREAMER_INCLUDE_DIRS}) +target_link_libraries(gstreamerViewer + PRIVATE + Qt::Core + Qt::Widgets + ${GSTREAMER_LIBRARIES} +) + +include(GNUInstallDirs) + +install(TARGETS gstreamerViewer + BUNDLE DESTINATION . + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} +) + +qt_generate_deploy_app_script( + TARGET gstreamerViewer + OUTPUT_SCRIPT deploy_script + NO_UNSUPPORTED_PLATFORM_ERROR +) +install(SCRIPT ${deploy_script}) diff --git a/README.md b/README.md new file mode 100644 index 0000000..23c24ae --- /dev/null +++ b/README.md @@ -0,0 +1,347 @@ +# GStreamer Viewer + +A Qt6-based GUI application for controlling and viewing video streams from cameras via the VizionStreamer backend. This application provides real-time camera control, GStreamer pipeline configuration, and video display capabilities. + +## Features + +- **Video Streaming Control**: Configure and start/stop camera streaming with GStreamer pipelines +- **Real-time Video Display**: View the streamed video in a separate window +- **Camera Parameter Control**: Adjust exposure, white balance, brightness, contrast, saturation, sharpness, gamma, and gain +- **Pipeline Presets**: Quick access to common pipeline configurations (MJPEG UDP, H.264 UDP, local display, etc.) +- **Format Detection**: Automatically fetch and select supported camera formats +- **Quick Start**: One-click auto-configuration and streaming +- **Unix Socket Communication**: Communicates with VizionStreamer backend via `/tmp/vizion_control.sock` + +## System Requirements + +- Linux (tested on Arch Linux) +- Qt6 +- GStreamer 1.0 +- VizionStreamer backend (not included) + +## Installation + +### Arch Linux + +Install the required packages using pacman: + +```bash +sudo pacman -S qt6-base gstreamer gst-plugins-base gst-plugins-good gst-plugins-bad cmake base-devel +``` + +**Package breakdown:** +- `qt6-base`: Qt6 framework (Widgets, Network, Core modules) +- `gstreamer`: GStreamer multimedia framework +- `gst-plugins-base`: Base GStreamer plugins (videoconvert, etc.) +- `gst-plugins-good`: Good quality plugins (JPEG encoding/decoding, RTP, UDP) +- `gst-plugins-bad`: Additional plugins (optional, for more formats) +- `cmake`: Build system +- `base-devel`: C++ compiler and build tools + +### Debian/Ubuntu + +Install the required packages using apt: + +```bash +sudo apt update +sudo apt install qt6-base-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ + gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-tools \ + cmake build-essential +``` + +**Package breakdown:** +- `qt6-base-dev`: Qt6 development files +- `libgstreamer1.0-dev`: GStreamer development headers +- `libgstreamer-plugins-base1.0-dev`: Base plugins development files +- `gstreamer1.0-plugins-good`: Good quality plugins runtime +- `gstreamer1.0-plugins-bad`: Additional plugins (optional) +- `gstreamer1.0-tools`: GStreamer command-line tools (for debugging with gst-launch-1.0) +- `cmake`: Build system +- `build-essential`: C++ compiler and build tools + +### Optional: H.264 Support + +For H.264 streaming (requires additional codec): + +**Arch Linux:** +```bash +sudo pacman -S gst-libav +``` + +**Debian/Ubuntu:** +```bash +sudo apt install gstreamer1.0-libav +``` + +## Building + +### Quick Build (using build script) + +The easiest way to build the project: + +```bash +cd /home/maik/project/gstreamerViewer +./build.sh +``` + +### Manual Build + +1. Clone or extract the project: +```bash +cd /home/maik/project/gstreamerViewer +``` + +2. Create a build directory: +```bash +mkdir -p build +cd build +``` + +3. Configure with CMake: +```bash +cmake .. +``` + +4. Build the project: +```bash +make -j$(nproc) +``` + +5. The executable will be located at: +```bash +./gstreamerViewer +``` + +## Usage + +### Prerequisites + +1. **VizionStreamer Backend**: Ensure the VizionStreamer backend is running and the Unix domain socket is available at `/tmp/vizion_control.sock` + +2. **Camera Connection**: Connect your camera (e.g., VCI-AR0234-C) and ensure VizionStreamer has selected the correct camera device + +### Quick Start Workflow + +The easiest way to start streaming: + +1. Launch the application: + ```bash + # Using the run script (checks for VizionStreamer) + ./run.sh + + # Or directly from build directory + cd build && ./gstreamerViewer + ``` + +2. Navigate to the **"GStreamer Pipeline"** tab + +3. Click the **"⚡ Quick Start (Auto Configure & Stream)"** button + - This automatically: + - Sets the camera format (1280x720@30fps UYVY or best available) + - Configures the MJPEG UDP streaming pipeline + - Starts the stream + +4. Switch to the **"Video Viewer"** tab + +5. Ensure the source type is set to **"UDP MJPEG Stream"** + +6. Click **"Start Viewer"** to display the video + +### Manual Configuration + +For more control over the streaming setup: + +#### Step 1: Configure Camera Format + +1. Go to the **"Camera Control"** tab +2. Click **"Get Available Formats"** to fetch supported formats from the camera +3. Select your desired format from the dropdown +4. Click **"Set Format"** + +#### Step 2: Configure Pipeline + +1. Go to the **"GStreamer Pipeline"** tab +2. Select a pipeline preset from the dropdown, or enter a custom pipeline: + - **MJPEG UDP Stream**: Best for raw formats (UYVY, YUY2), no additional plugins needed + - **UDP H.264 Stream**: Requires gst-libav, better compression + - **Local Display**: For testing (shows video on server side) +3. Click **"Set Pipeline"** + +#### Step 3: Start Streaming + +1. Click **"Start Stream"** in the GStreamer Pipeline tab +2. The status should show "Status: Streaming" with a green background + +#### Step 4: View the Stream + +1. Go to the **"Video Viewer"** tab +2. Select the appropriate source type (matches your pipeline): + - **UDP MJPEG Stream** for MJPEG UDP pipeline + - **UDP H.264 Stream** for H.264 UDP pipeline +3. Verify host/port settings (default: port 5000) +4. Click **"Start Viewer"** +5. Video will appear in a separate window + +### Camera Control + +The **"Camera Control"** tab provides real-time adjustment of camera parameters: + +- **Exposure**: Auto or Manual mode with adjustable value +- **White Balance**: Auto or Manual with temperature control (2800-6500K) +- **Image Adjustments**: + - Brightness (0-255) + - Contrast (0-255) + - Saturation (0-255) + - Sharpness (0-255) + - Gamma (72-500) + - Gain (0-100) + +All slider changes are applied immediately to the camera. + +## Pipeline Presets Explained + +### MJPEG UDP Stream +``` +videoconvert ! jpegenc ! rtpjpegpay ! udpsink host=127.0.0.1 port=5000 +``` +- **Best for**: Raw formats (UYVY, YUY2, RGB) +- **Pros**: No additional plugins needed, reliable +- **Cons**: Lower compression than H.264 + +### UDP H.264 Stream +``` +videoconvert ! x264enc tune=zerolatency ! rtph264pay ! udpsink host=127.0.0.1 port=5000 +``` +- **Best for**: Higher compression, lower bandwidth +- **Pros**: Better compression +- **Cons**: Requires gst-libav plugin + +### Local Display +``` +videoconvert ! autovideosink +``` +- **Best for**: Testing camera without network streaming +- **Shows**: Video on the server machine + +## Troubleshooting + +### Issue: "Failed to start streaming" + +**Solution**: Ensure the camera format is set before starting the stream: +1. Click "Get Available Formats" in Camera Control tab +2. Select a supported format +3. Click "Set Format" +4. Try starting the stream again + +### Issue: Video is black/not displaying + +**Possible causes:** +1. **Pipeline mismatch**: Ensure the viewer source type matches the streaming pipeline +2. **No UDP packets**: Verify with tcpdump: + ```bash + sudo tcpdump -i lo udp port 5000 -nn + ``` +3. **Wrong camera selected**: Check VizionStreamer logs to ensure correct camera is active + +### Issue: "No element 'avdec_h264'" + +**Solution**: Install the gst-libav plugin or use the MJPEG UDP pipeline instead + +**Arch Linux:** +```bash +sudo pacman -S gst-libav +``` + +**Debian/Ubuntu:** +```bash +sudo apt install gstreamer1.0-libav +``` + +### Issue: Connection error to VizionStreamer + +**Solution**: Verify the backend is running: +```bash +ls -la /tmp/vizion_control.sock +``` + +If the socket doesn't exist, start VizionStreamer. + +**Test the connection**: +```bash +./test_connection.sh +``` + +This script will verify the socket exists and test basic communication with VizionStreamer. + +### Issue: X11 BadWindow errors with video display + +**Note**: The application uses `autovideosink` which opens a separate video window. This is intentional due to X11 limitations with embedded video overlays in Qt. + +### Debug: Test pipeline manually + +Test if GStreamer can receive the stream: +```bash +gst-launch-1.0 udpsrc port=5000 ! application/x-rtp,encoding-name=JPEG,payload=26 ! rtpjpegdepay ! jpegdec ! autovideosink +``` + +## Supported Cameras + +This application works with cameras supported by the VizionStreamer backend, including: +- VCI-AR0234-C (tested: UYVY at 1920x1200@60fps) +- Other V4L2-compatible cameras + +## Architecture + +``` +┌─────────────────────┐ +│ gstreamerViewer │ +│ (Qt6 GUI) │ +└──────────┬──────────┘ + │ Unix Socket (/tmp/vizion_control.sock) + │ JSON Commands + ▼ +┌─────────────────────┐ +│ VizionStreamer │ +│ Backend │ +└──────────┬──────────┘ + │ V4L2 / VizionSDK + ▼ +┌─────────────────────┐ +│ Camera Hardware │ +│ (VCI-AR0234-C) │ +└─────────────────────┘ + +Video Stream Flow: +Camera → VizionStreamer → GStreamer Pipeline → UDP/Local → VideoViewer +``` + +## Socket API Commands + +The application communicates with VizionStreamer using JSON commands. See `SOCKET_API.md` for full protocol documentation. + +Example commands: +- `get_formats`: Retrieve available camera formats +- `set_format`: Set camera resolution, framerate, and pixel format +- `set_pipeline`: Configure GStreamer pipeline +- `start_stream`: Start camera streaming +- `stop_stream`: Stop camera streaming +- `get_status`: Query streaming status +- `set_exposure`, `set_brightness`, etc.: Camera parameter controls + +## License + +[Specify your license here] + +## Credits + +Built with: +- Qt6 Framework +- GStreamer Multimedia Framework +- VizionSDK + +## Support + +For issues or questions: +1. Check the Troubleshooting section above +2. Verify VizionStreamer backend is running correctly +3. Test GStreamer pipelines manually with `gst-launch-1.0` diff --git a/SOCKET_API.md b/SOCKET_API.md new file mode 100644 index 0000000..8a5afb4 --- /dev/null +++ b/SOCKET_API.md @@ -0,0 +1,663 @@ +# VizionStreamer Socket Control API + +VizionStreamer can be controlled via a Unix Domain Socket interface. This allows external applications to configure camera parameters and stream settings at runtime. + +## Socket Connection + +- **Socket Path**: `/tmp/vizion_control.sock` +- **Protocol**: Unix Domain Socket (SOCK_STREAM) +- **Message Format**: JSON + +## Command Format + +All commands follow this JSON structure: + +```json +{ + "command": "command_name", + "params": { + "param1": "value1", + "param2": "value2" + } +} +``` + +## Response Format + +All responses follow this JSON structure: + +**Success Response:** +```json +{ + "status": "success", + "message": "Optional success message" +} +``` + +**Error Response:** +```json +{ + "status": "error", + "message": "Error description" +} +``` + +## Available Commands + +### 1. Get Available Formats + +Retrieve all supported video formats. + +**Command:** +```json +{ + "command": "get_formats" +} +``` + +**Response:** +```json +{ + "status": "success", + "formats": [ + { + "width": 1920, + "height": 1080, + "framerate": 30, + "format": "YUY2" + }, + { + "width": 1280, + "height": 720, + "framerate": 60, + "format": "MJPG" + } + ] +} +``` + +**Supported Formats:** YUY2, UYVY, NV12, MJPG, BGR, RGB + +--- + +### 2. Set Video Format + +Change the video format (resolution, framerate, pixel format). + +**Note:** Cannot be changed while streaming is active. + +**Command:** +```json +{ + "command": "set_format", + "params": { + "width": "1920", + "height": "1080", + "framerate": "30", + "format": "YUY2" + } +} +``` + +**Response:** +```json +{ + "status": "success", + "message": "Format set successfully" +} +``` + +--- + +### 3. Start Streaming + +Start video streaming from the camera. + +**Command:** +```json +{ + "command": "start_stream" +} +``` + +**Response:** +```json +{ + "status": "success", + "message": "Streaming started" +} +``` + +--- + +### 4. Stop Streaming + +Stop video streaming. + +**Command:** +```json +{ + "command": "stop_stream" +} +``` + +**Response:** +```json +{ + "status": "success", + "message": "Streaming stopped" +} +``` + +--- + +### 5. Set GStreamer Pipeline + +Configure the GStreamer pipeline for video output. This determines where and how the video stream is processed/displayed. + +**Note:** Cannot be changed while streaming is active. + +**Command:** +```json +{ + "command": "set_pipeline", + "params": { + "pipeline": "videoconvert ! x264enc ! rtph264pay ! udpsink host=192.168.1.100 port=5000" + } +} +``` + +**Response:** +```json +{ + "status": "success", + "message": "Pipeline set successfully" +} +``` + +**Common Pipeline Examples:** + +1. **Display locally:** + ``` + videoconvert ! autovideosink + ``` + +2. **Stream over UDP (H.264):** + ``` + videoconvert ! x264enc tune=zerolatency ! rtph264pay ! udpsink host=192.168.1.100 port=5000 + ``` + +3. **Stream over RTSP (requires gst-rtsp-server):** + ``` + videoconvert ! x264enc ! rtph264pay name=pay0 + ``` + +4. **Save to file:** + ``` + videoconvert ! x264enc ! mp4mux ! filesink location=/tmp/output.mp4 + ``` + +5. **Stream over TCP:** + ``` + videoconvert ! x264enc ! h264parse ! mpegtsmux ! tcpserversink host=0.0.0.0 port=5000 + ``` + +6. **MJPEG over HTTP:** + ``` + videoconvert ! jpegenc ! multipartmux ! tcpserversink host=0.0.0.0 port=8080 + ``` + +--- + +### 6. Get Status + +Get current streaming status and pipeline configuration. + +**Command:** +```json +{ + "command": "get_status" +} +``` + +**Response:** +```json +{ + "status": "success", + "streaming": true, + "pipeline": "videoconvert ! autovideosink" +} +``` + +--- + +### 7. Set Exposure + +Configure camera exposure settings. + +**Command:** +```json +{ + "command": "set_exposure", + "params": { + "mode": "manual", + "value": "100" + } +} +``` + +**Parameters:** +- `mode`: "auto" or "manual" +- `value`: Exposure value (only used in manual mode) + +**Response:** +```json +{ + "status": "success", + "message": "Exposure set successfully" +} +``` + +--- + +### 8. Set White Balance + +Configure white balance settings. + +**Command:** +```json +{ + "command": "set_whitebalance", + "params": { + "mode": "auto", + "temperature": "4500" + } +} +``` + +**Parameters:** +- `mode`: "auto" or "manual" +- `temperature`: Color temperature in Kelvin (only used in manual mode) + +**Response:** +```json +{ + "status": "success", + "message": "White balance set successfully" +} +``` + +--- + +### 9. Set Brightness + +Adjust camera brightness. + +**Command:** +```json +{ + "command": "set_brightness", + "params": { + "value": "50" + } +} +``` + +**Response:** +```json +{ + "status": "success", + "message": "Brightness set successfully" +} +``` + +--- + +### 10. Set Contrast + +Adjust camera contrast. + +**Command:** +```json +{ + "command": "set_contrast", + "params": { + "value": "32" + } +} +``` + +**Response:** +```json +{ + "status": "success", + "message": "Contrast set successfully" +} +``` + +--- + +### 11. Set Saturation + +Adjust color saturation. + +**Command:** +```json +{ + "command": "set_saturation", + "params": { + "value": "64" + } +} +``` + +**Response:** +```json +{ + "status": "success", + "message": "Saturation set successfully" +} +``` + +--- + +### 12. Set Sharpness + +Adjust image sharpness. + +**Command:** +```json +{ + "command": "set_sharpness", + "params": { + "value": "3" + } +} +``` + +**Response:** +```json +{ + "status": "success", + "message": "Sharpness set successfully" +} +``` + +--- + +### 13. Set Gamma + +Adjust gamma correction. + +**Command:** +```json +{ + "command": "set_gamma", + "params": { + "value": "100" + } +} +``` + +**Response:** +```json +{ + "status": "success", + "message": "Gamma set successfully" +} +``` + +--- + +### 14. Set Gain + +Adjust camera gain. + +**Command:** +```json +{ + "command": "set_gain", + "params": { + "value": "0" + } +} +``` + +**Response:** +```json +{ + "status": "success", + "message": "Gain set successfully" +} +``` + +--- + +## Usage Examples + +### Complete Workflow Example + +```bash +# 1. Set GStreamer pipeline for UDP streaming +echo '{"command":"set_pipeline","params":{"pipeline":"videoconvert ! x264enc tune=zerolatency ! rtph264pay ! udpsink host=192.168.1.100 port=5000"}}' | socat - UNIX-CONNECT:/tmp/vizion_control.sock + +# 2. Set video format +echo '{"command":"set_format","params":{"width":"1920","height":"1080","framerate":"30","format":"YUY2"}}' | socat - UNIX-CONNECT:/tmp/vizion_control.sock + +# 3. Configure camera settings +echo '{"command":"set_exposure","params":{"mode":"auto"}}' | socat - UNIX-CONNECT:/tmp/vizion_control.sock +echo '{"command":"set_brightness","params":{"value":"50"}}' | socat - UNIX-CONNECT:/tmp/vizion_control.sock + +# 4. Start streaming +echo '{"command":"start_stream"}' | socat - UNIX-CONNECT:/tmp/vizion_control.sock + +# 5. Check status +echo '{"command":"get_status"}' | socat - UNIX-CONNECT:/tmp/vizion_control.sock + +# 6. Stop streaming when done +echo '{"command":"stop_stream"}' | socat - UNIX-CONNECT:/tmp/vizion_control.sock +``` + +### GStreamer Pipeline Examples + +```bash +# Stream to local display +echo '{"command":"set_pipeline","params":{"pipeline":"videoconvert ! autovideosink"}}' | socat - UNIX-CONNECT:/tmp/vizion_control.sock + +# Stream over UDP (H.264) +echo '{"command":"set_pipeline","params":{"pipeline":"videoconvert ! x264enc tune=zerolatency ! rtph264pay ! udpsink host=192.168.1.100 port=5000"}}' | socat - UNIX-CONNECT:/tmp/vizion_control.sock + +# Save to MP4 file +echo '{"command":"set_pipeline","params":{"pipeline":"videoconvert ! x264enc ! mp4mux ! filesink location=/tmp/output.mp4"}}' | socat - UNIX-CONNECT:/tmp/vizion_control.sock + +# MJPEG HTTP server +echo '{"command":"set_pipeline","params":{"pipeline":"videoconvert ! jpegenc ! multipartmux ! tcpserversink host=0.0.0.0 port=8080"}}' | socat - UNIX-CONNECT:/tmp/vizion_control.sock +``` + +### Using `socat` + +```bash +# Get available formats +echo '{"command":"get_formats"}' | socat - UNIX-CONNECT:/tmp/vizion_control.sock + +# Set video format +echo '{"command":"set_format","params":{"width":"1920","height":"1080","framerate":"30","format":"YUY2"}}' | socat - UNIX-CONNECT:/tmp/vizion_control.sock + +# Start streaming +echo '{"command":"start_stream"}' | socat - UNIX-CONNECT:/tmp/vizion_control.sock + +# Set exposure to auto +echo '{"command":"set_exposure","params":{"mode":"auto"}}' | socat - UNIX-CONNECT:/tmp/vizion_control.sock + +# Set brightness +echo '{"command":"set_brightness","params":{"value":"50"}}' | socat - UNIX-CONNECT:/tmp/vizion_control.sock + +# Get status +echo '{"command":"get_status"}' | socat - UNIX-CONNECT:/tmp/vizion_control.sock + +# Stop streaming +echo '{"command":"stop_stream"}' | socat - UNIX-CONNECT:/tmp/vizion_control.sock +``` + +### Using `nc` (netcat with Unix socket support) + +```bash +echo '{"command":"get_formats"}' | nc -U /tmp/vizion_control.sock +``` + +### Using Python + +```python +import socket +import json + +def send_command(command, params=None): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect('/tmp/vizion_control.sock') + + cmd = {"command": command} + if params: + cmd["params"] = params + + sock.send(json.dumps(cmd).encode()) + response = sock.recv(4096).decode() + sock.close() + + return json.loads(response) + +# Examples +print(send_command("get_formats")) +print(send_command("set_format", { + "width": "1920", + "height": "1080", + "framerate": "30", + "format": "YUY2" +})) +print(send_command("set_exposure", {"mode": "auto"})) +print(send_command("start_stream")) +``` + +### Using C++ + +```cpp +#include +#include +#include +#include +#include + +std::string sendCommand(const std::string& command) { + int sock = socket(AF_UNIX, SOCK_STREAM, 0); + + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strcpy(addr.sun_path, "/tmp/vizion_control.sock"); + + connect(sock, (struct sockaddr*)&addr, sizeof(addr)); + send(sock, command.c_str(), command.length(), 0); + + char buffer[4096]; + int bytesRead = recv(sock, buffer, sizeof(buffer) - 1, 0); + buffer[bytesRead] = '\0'; + + close(sock); + return std::string(buffer); +} + +// Example usage +int main() { + std::cout << sendCommand(R"({"command":"get_formats"})") << std::endl; + std::cout << sendCommand(R"({"command":"set_brightness","params":{"value":"50"}})") << std::endl; + return 0; +} +``` + +## Parameter Value Ranges + +The valid ranges for camera parameters depend on the specific camera model. You can query the camera capabilities through the VizionSDK API or experimentally determine valid ranges. + +**Typical ranges (camera-dependent):** +- Brightness: 0-255 +- Contrast: 0-255 +- Saturation: 0-255 +- Sharpness: 0-255 +- Gamma: 72-500 +- Gain: 0-100 +- Exposure: 1-10000 (in auto mode, value is ignored) +- White Balance Temperature: 2800-6500 Kelvin + +## Error Handling + +Always check the `status` field in the response: + +```python +response = send_command("set_format", {...}) +if response["status"] == "error": + print(f"Command failed: {response['message']}") +else: + print("Command successful") +``` + +## Thread Safety + +The socket server handles one client connection at a time. Commands are processed sequentially with mutex protection to ensure thread safety with the camera operations. + +## GStreamer Integration + +VizionStreamer uses GStreamer for video processing and output. The captured frames from the VizionSDK camera are continuously fed into a GStreamer pipeline in a separate acquisition thread. + +### How It Works + +1. **Continuous Acquisition Loop**: A dedicated thread continuously captures frames from the camera using `VxGetImage()` +2. **Frame Buffering**: Captured frames are pushed into the GStreamer pipeline via `appsrc` +3. **Pipeline Processing**: GStreamer processes the frames according to the configured pipeline +4. **Output**: Frames are displayed, saved, or streamed based on the pipeline configuration + +### Performance Monitoring + +The acquisition loop prints FPS statistics every second: +``` +FPS: 30 | Total frames: 1234 | Frame size: 4147200 bytes +``` + +### Receiving UDP Stream + +If you configured a UDP streaming pipeline, receive it with: + +```bash +# Using GStreamer +gst-launch-1.0 udpsrc port=5000 ! application/x-rtp,encoding-name=H264 ! rtph264depay ! h264parse ! avdec_h264 ! videoconvert ! autovideosink + +# Using FFplay +ffplay -fflags nobuffer -flags low_delay -framedrop udp://0.0.0.0:5000 + +# Using VLC +vlc udp://@:5000 +``` + +### Receiving MJPEG HTTP Stream + +If you configured an MJPEG HTTP server pipeline: + +```bash +# View in browser +firefox http://192.168.1.100:8080 + +# Using FFplay +ffplay http://192.168.1.100:8080 + +# Using curl to save frames +curl http://192.168.1.100:8080 > stream.mjpg +``` + +## Notes + +- The socket file is automatically created when VizionStreamer starts +- The socket file is removed when VizionStreamer exits cleanly +- Format and pipeline changes require streaming to be stopped first +- The acquisition loop runs continuously while streaming is active +- Some parameters may not be supported on all camera models +- Invalid parameter values will return an error response +- GStreamer pipeline errors will be reported when starting the stream +- Default pipeline: `videoconvert ! autovideosink` (display locally) diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..bdc0a6b --- /dev/null +++ b/build.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Build script for gstreamerViewer + +set -e # Exit on error + +echo "=== Building gstreamerViewer ===" + +# Create build directory if it doesn't exist +if [ ! -d "build" ]; then + echo "Creating build directory..." + mkdir -p build +fi + +cd build + +echo "Running CMake..." +cmake .. + +echo "Building with make..." +make -j$(nproc) + +echo "" +echo "=== Build successful! ===" +echo "Executable: $(pwd)/gstreamerViewer" +echo "" +echo "To run the application:" +echo " cd build && ./gstreamerViewer" +echo "" diff --git a/cameracontrolwidget.cpp b/cameracontrolwidget.cpp new file mode 100644 index 0000000..7825924 --- /dev/null +++ b/cameracontrolwidget.cpp @@ -0,0 +1,381 @@ +#include "cameracontrolwidget.h" +#include +#include +#include +#include +#include +#include + +CameraControlWidget::CameraControlWidget(SocketClient* socketClient, QWidget *parent) + : QWidget(parent), m_socketClient(socketClient) +{ + setupUI(); +} + +void CameraControlWidget::setupUI() +{ + QVBoxLayout* mainLayout = new QVBoxLayout(this); + + // Create scroll area for all controls + QScrollArea* scrollArea = new QScrollArea(this); + scrollArea->setWidgetResizable(true); + scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + QWidget* scrollWidget = new QWidget(); + QVBoxLayout* scrollLayout = new QVBoxLayout(scrollWidget); + + // Add all control groups + scrollLayout->addWidget(createFormatGroup()); + scrollLayout->addWidget(createExposureGroup()); + scrollLayout->addWidget(createWhiteBalanceGroup()); + scrollLayout->addWidget(createImageAdjustmentGroup()); + scrollLayout->addStretch(); + + scrollWidget->setLayout(scrollLayout); + scrollArea->setWidget(scrollWidget); + + mainLayout->addWidget(scrollArea); + + // Status label at bottom + m_statusLabel = new QLabel("Status: Ready", this); + m_statusLabel->setStyleSheet("QLabel { background-color: #f0f0f0; padding: 5px; border-radius: 3px; }"); + mainLayout->addWidget(m_statusLabel); + + setLayout(mainLayout); +} + +QGroupBox* CameraControlWidget::createFormatGroup() +{ + QGroupBox* groupBox = new QGroupBox("Video Format", this); + QVBoxLayout* layout = new QVBoxLayout(); + + m_formatCombo = new QComboBox(this); + m_formatCombo->addItem("1280x720@30fps UYVY (Supported)", "1280,720,30,UYVY"); + + m_getFormatsBtn = new QPushButton("Get Available Formats", this); + m_setFormatBtn = new QPushButton("Set Format", this); + + connect(m_getFormatsBtn, &QPushButton::clicked, this, &CameraControlWidget::onGetFormats); + connect(m_setFormatBtn, &QPushButton::clicked, this, &CameraControlWidget::onSetFormat); + + layout->addWidget(new QLabel("Select Format:", this)); + layout->addWidget(m_formatCombo); + layout->addWidget(m_getFormatsBtn); + layout->addWidget(m_setFormatBtn); + + groupBox->setLayout(layout); + return groupBox; +} + +QGroupBox* CameraControlWidget::createExposureGroup() +{ + QGroupBox* groupBox = new QGroupBox("Exposure", this); + QVBoxLayout* layout = new QVBoxLayout(); + + QButtonGroup* exposureGroup = new QButtonGroup(this); + m_exposureAuto = new QRadioButton("Auto", this); + m_exposureManual = new QRadioButton("Manual", this); + m_exposureAuto->setChecked(true); + + exposureGroup->addButton(m_exposureAuto); + exposureGroup->addButton(m_exposureManual); + + connect(m_exposureAuto, &QRadioButton::toggled, this, &CameraControlWidget::onExposureModeChanged); + + QHBoxLayout* modeLayout = new QHBoxLayout(); + modeLayout->addWidget(m_exposureAuto); + modeLayout->addWidget(m_exposureManual); + + m_exposureValue = new QSpinBox(this); + m_exposureValue->setRange(1, 10000); + m_exposureValue->setValue(100); + m_exposureValue->setEnabled(false); + + m_setExposureBtn = new QPushButton("Set Exposure", this); + connect(m_setExposureBtn, &QPushButton::clicked, this, &CameraControlWidget::onSetExposure); + + QFormLayout* formLayout = new QFormLayout(); + formLayout->addRow("Mode:", modeLayout); + formLayout->addRow("Value:", m_exposureValue); + + layout->addLayout(formLayout); + layout->addWidget(m_setExposureBtn); + + groupBox->setLayout(layout); + return groupBox; +} + +QGroupBox* CameraControlWidget::createWhiteBalanceGroup() +{ + QGroupBox* groupBox = new QGroupBox("White Balance", this); + QVBoxLayout* layout = new QVBoxLayout(); + + QButtonGroup* wbGroup = new QButtonGroup(this); + m_whiteBalanceAuto = new QRadioButton("Auto", this); + m_whiteBalanceManual = new QRadioButton("Manual", this); + m_whiteBalanceAuto->setChecked(true); + + wbGroup->addButton(m_whiteBalanceAuto); + wbGroup->addButton(m_whiteBalanceManual); + + connect(m_whiteBalanceAuto, &QRadioButton::toggled, this, &CameraControlWidget::onWhiteBalanceModeChanged); + + QHBoxLayout* modeLayout = new QHBoxLayout(); + modeLayout->addWidget(m_whiteBalanceAuto); + modeLayout->addWidget(m_whiteBalanceManual); + + m_whiteBalanceTemp = new QSpinBox(this); + m_whiteBalanceTemp->setRange(2800, 6500); + m_whiteBalanceTemp->setValue(4500); + m_whiteBalanceTemp->setSuffix(" K"); + m_whiteBalanceTemp->setEnabled(false); + + m_setWhiteBalanceBtn = new QPushButton("Set White Balance", this); + connect(m_setWhiteBalanceBtn, &QPushButton::clicked, this, &CameraControlWidget::onSetWhiteBalance); + + QFormLayout* formLayout = new QFormLayout(); + formLayout->addRow("Mode:", modeLayout); + formLayout->addRow("Temperature:", m_whiteBalanceTemp); + + layout->addLayout(formLayout); + layout->addWidget(m_setWhiteBalanceBtn); + + groupBox->setLayout(layout); + return groupBox; +} + +QGroupBox* CameraControlWidget::createImageAdjustmentGroup() +{ + QGroupBox* groupBox = new QGroupBox("Image Adjustments", this); + QVBoxLayout* layout = new QVBoxLayout(); + + layout->addWidget(createSliderControl("Brightness (0-255):", 0, 255, 128, + &m_brightnessSlider, &m_brightnessSpinBox)); + connect(m_brightnessSlider, &QSlider::valueChanged, this, &CameraControlWidget::onBrightnessChanged); + + layout->addWidget(createSliderControl("Contrast (0-255):", 0, 255, 32, + &m_contrastSlider, &m_contrastSpinBox)); + connect(m_contrastSlider, &QSlider::valueChanged, this, &CameraControlWidget::onContrastChanged); + + layout->addWidget(createSliderControl("Saturation (0-255):", 0, 255, 64, + &m_saturationSlider, &m_saturationSpinBox)); + connect(m_saturationSlider, &QSlider::valueChanged, this, &CameraControlWidget::onSaturationChanged); + + layout->addWidget(createSliderControl("Sharpness (0-255):", 0, 255, 3, + &m_sharpnessSlider, &m_sharpnessSpinBox)); + connect(m_sharpnessSlider, &QSlider::valueChanged, this, &CameraControlWidget::onSharpnessChanged); + + layout->addWidget(createSliderControl("Gamma (72-500):", 72, 500, 100, + &m_gammaSlider, &m_gammaSpinBox)); + connect(m_gammaSlider, &QSlider::valueChanged, this, &CameraControlWidget::onGammaChanged); + + layout->addWidget(createSliderControl("Gain (0-100):", 0, 100, 0, + &m_gainSlider, &m_gainSpinBox)); + connect(m_gainSlider, &QSlider::valueChanged, this, &CameraControlWidget::onGainChanged); + + groupBox->setLayout(layout); + return groupBox; +} + +QWidget* CameraControlWidget::createSliderControl(const QString& label, int min, int max, int defaultValue, + QSlider** slider, QSpinBox** spinBox) +{ + QWidget* widget = new QWidget(this); + QVBoxLayout* layout = new QVBoxLayout(widget); + layout->setContentsMargins(0, 5, 0, 5); + + QLabel* titleLabel = new QLabel(label, this); + + QHBoxLayout* controlLayout = new QHBoxLayout(); + + *slider = new QSlider(Qt::Horizontal, this); + (*slider)->setRange(min, max); + (*slider)->setValue(defaultValue); + + *spinBox = new QSpinBox(this); + (*spinBox)->setRange(min, max); + (*spinBox)->setValue(defaultValue); + + connect(*slider, &QSlider::valueChanged, *spinBox, &QSpinBox::setValue); + connect(*spinBox, QOverload::of(&QSpinBox::valueChanged), *slider, &QSlider::setValue); + + controlLayout->addWidget(*slider, 1); + controlLayout->addWidget(*spinBox); + + layout->addWidget(titleLabel); + layout->addLayout(controlLayout); + + return widget; +} + +void CameraControlWidget::onGetFormats() +{ + m_socketClient->sendCommand("get_formats", QJsonObject(), + [this](const QJsonObject& response) { + if (response.contains("formats")) { + QJsonArray formats = response["formats"].toArray(); + m_formatCombo->clear(); + + for (const QJsonValue& val : formats) { + QJsonObject fmt = val.toObject(); + int width = fmt["width"].toInt(); + int height = fmt["height"].toInt(); + int fps = fmt["framerate"].toInt(); + QString format = fmt["format"].toString(); + + QString displayText = QString("%1x%2@%3fps %4") + .arg(width).arg(height).arg(fps).arg(format); + QString data = QString("%1,%2,%3,%4").arg(width).arg(height).arg(fps).arg(format); + + m_formatCombo->addItem(displayText, data); + } + + updateStatus(QString("Found %1 available formats").arg(formats.size()), true); + } + }, + [this](const QString& error) { + updateStatus("Error: Failed to get formats: " + error, false); + }); +} + +void CameraControlWidget::onSetFormat() +{ + QString data = m_formatCombo->currentData().toString(); + QStringList parts = data.split(','); + + if (parts.size() != 4) { + updateStatus("Error: Invalid format selection", false); + return; + } + + QJsonObject params; + params["width"] = parts[0]; + params["height"] = parts[1]; + params["framerate"] = parts[2]; + params["format"] = parts[3]; + + m_socketClient->sendCommand("set_format", params, + [this](const QJsonObject& response) { + updateStatus("Format set successfully", true); + }, + [this](const QString& error) { + updateStatus("Error: Failed to set format: " + error, false); + }); +} + +void CameraControlWidget::onSetExposure() +{ + QJsonObject params; + params["mode"] = m_exposureAuto->isChecked() ? "auto" : "manual"; + if (m_exposureManual->isChecked()) { + params["value"] = QString::number(m_exposureValue->value()); + } + + m_socketClient->sendCommand("set_exposure", params, + [this](const QJsonObject& response) { + updateStatus("Exposure set successfully", true); + }, + [this](const QString& error) { + updateStatus("Error: Failed to set exposure: " + error, false); + }); +} + +void CameraControlWidget::onSetWhiteBalance() +{ + QJsonObject params; + params["mode"] = m_whiteBalanceAuto->isChecked() ? "auto" : "manual"; + if (m_whiteBalanceManual->isChecked()) { + params["temperature"] = QString::number(m_whiteBalanceTemp->value()); + } + + m_socketClient->sendCommand("set_whitebalance", params, + [this](const QJsonObject& response) { + updateStatus("White balance set successfully", true); + }, + [this](const QString& error) { + updateStatus("Error: Failed to set white balance: " + error, false); + }); +} + +void CameraControlWidget::onBrightnessChanged(int value) +{ + QJsonObject params; + params["value"] = QString::number(value); + + m_socketClient->sendCommand("set_brightness", params, + [](const QJsonObject&) {}, + [](const QString&) {}); +} + +void CameraControlWidget::onContrastChanged(int value) +{ + QJsonObject params; + params["value"] = QString::number(value); + + m_socketClient->sendCommand("set_contrast", params, + [](const QJsonObject&) {}, + [](const QString&) {}); +} + +void CameraControlWidget::onSaturationChanged(int value) +{ + QJsonObject params; + params["value"] = QString::number(value); + + m_socketClient->sendCommand("set_saturation", params, + [](const QJsonObject&) {}, + [](const QString&) {}); +} + +void CameraControlWidget::onSharpnessChanged(int value) +{ + QJsonObject params; + params["value"] = QString::number(value); + + m_socketClient->sendCommand("set_sharpness", params, + [](const QJsonObject&) {}, + [](const QString&) {}); +} + +void CameraControlWidget::onGammaChanged(int value) +{ + QJsonObject params; + params["value"] = QString::number(value); + + m_socketClient->sendCommand("set_gamma", params, + [](const QJsonObject&) {}, + [](const QString&) {}); +} + +void CameraControlWidget::onGainChanged(int value) +{ + QJsonObject params; + params["value"] = QString::number(value); + + m_socketClient->sendCommand("set_gain", params, + [](const QJsonObject&) {}, + [](const QString&) {}); +} + +void CameraControlWidget::onExposureModeChanged() +{ + m_exposureValue->setEnabled(m_exposureManual->isChecked()); +} + +void CameraControlWidget::onWhiteBalanceModeChanged() +{ + m_whiteBalanceTemp->setEnabled(m_whiteBalanceManual->isChecked()); +} + +void CameraControlWidget::updateStatus(const QString& status, bool isSuccess) +{ + m_statusLabel->setText("Status: " + status); + + if (isSuccess) { + m_statusLabel->setStyleSheet("QLabel { background-color: #90EE90; padding: 5px; border-radius: 3px; }"); + } else if (status.startsWith("Error")) { + m_statusLabel->setStyleSheet("QLabel { background-color: #FFB6C1; padding: 5px; border-radius: 3px; }"); + } else { + m_statusLabel->setStyleSheet("QLabel { background-color: #f0f0f0; padding: 5px; border-radius: 3px; }"); + } +} diff --git a/cameracontrolwidget.h b/cameracontrolwidget.h new file mode 100644 index 0000000..3fa6c70 --- /dev/null +++ b/cameracontrolwidget.h @@ -0,0 +1,88 @@ +#ifndef CAMERACONTROLWIDGET_H +#define CAMERACONTROLWIDGET_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include "socketclient.h" + +class CameraControlWidget : public QWidget +{ + Q_OBJECT + +public: + explicit CameraControlWidget(SocketClient* socketClient, QWidget *parent = nullptr); + +private slots: + void onGetFormats(); + void onSetFormat(); + void onSetExposure(); + void onSetWhiteBalance(); + void onBrightnessChanged(int value); + void onContrastChanged(int value); + void onSaturationChanged(int value); + void onSharpnessChanged(int value); + void onGammaChanged(int value); + void onGainChanged(int value); + void onExposureModeChanged(); + void onWhiteBalanceModeChanged(); + +private: + void setupUI(); + QGroupBox* createFormatGroup(); + QGroupBox* createExposureGroup(); + QGroupBox* createWhiteBalanceGroup(); + QGroupBox* createImageAdjustmentGroup(); + QWidget* createSliderControl(const QString& label, int min, int max, int defaultValue, + QSlider** slider, QSpinBox** spinBox); + + SocketClient* m_socketClient; + + // Format controls + QComboBox* m_formatCombo; + QPushButton* m_getFormatsBtn; + QPushButton* m_setFormatBtn; + + // Exposure controls + QRadioButton* m_exposureAuto; + QRadioButton* m_exposureManual; + QSpinBox* m_exposureValue; + QPushButton* m_setExposureBtn; + + // White Balance controls + QRadioButton* m_whiteBalanceAuto; + QRadioButton* m_whiteBalanceManual; + QSpinBox* m_whiteBalanceTemp; + QPushButton* m_setWhiteBalanceBtn; + + // Image adjustment controls + QSlider* m_brightnessSlider; + QSpinBox* m_brightnessSpinBox; + + QSlider* m_contrastSlider; + QSpinBox* m_contrastSpinBox; + + QSlider* m_saturationSlider; + QSpinBox* m_saturationSpinBox; + + QSlider* m_sharpnessSlider; + QSpinBox* m_sharpnessSpinBox; + + QSlider* m_gammaSlider; + QSpinBox* m_gammaSpinBox; + + QSlider* m_gainSlider; + QSpinBox* m_gainSpinBox; + + // Status display + QLabel* m_statusLabel; + + void updateStatus(const QString& status, bool isSuccess); +}; + +#endif // CAMERACONTROLWIDGET_H diff --git a/gstreamerpipelinewidget.cpp b/gstreamerpipelinewidget.cpp new file mode 100644 index 0000000..83a87cd --- /dev/null +++ b/gstreamerpipelinewidget.cpp @@ -0,0 +1,328 @@ +#include "gstreamerpipelinewidget.h" +#include +#include +#include +#include +#include +#include + +GStreamerPipelineWidget::GStreamerPipelineWidget(SocketClient* socketClient, QWidget *parent) + : QWidget(parent), m_socketClient(socketClient) +{ + setupUI(); + onGetStatus(); + fetchAvailableFormats(); +} + +void GStreamerPipelineWidget::setupUI() +{ + QVBoxLayout* mainLayout = new QVBoxLayout(this); + + QGroupBox* groupBox = new QGroupBox("GStreamer Pipeline", this); + QVBoxLayout* groupLayout = new QVBoxLayout(groupBox); + + // Info label with instructions + m_infoLabel = new QLabel( + "Quick Start: Click 'Quick Start' to automatically configure and start streaming.
" + "Manual: 1. Set video format → 2. Set pipeline → 3. Start stream", this); + m_infoLabel->setStyleSheet("QLabel { background-color: #e3f2fd; padding: 8px; border-radius: 4px; }"); + m_infoLabel->setWordWrap(true); + groupLayout->addWidget(m_infoLabel); + + // Quick Start button (prominent) + m_quickStartBtn = new QPushButton("⚡ Quick Start (Auto Configure & Stream)", this); + m_quickStartBtn->setStyleSheet("QPushButton { background-color: #4CAF50; color: white; font-weight: bold; padding: 10px; }"); + connect(m_quickStartBtn, &QPushButton::clicked, this, &GStreamerPipelineWidget::onQuickStart); + groupLayout->addWidget(m_quickStartBtn); + + // Separator + QFrame* line = new QFrame(this); + line->setFrameShape(QFrame::HLine); + line->setFrameShadow(QFrame::Sunken); + groupLayout->addWidget(line); + + // Format selection + QLabel* formatLabel = new QLabel("Video Format:", this); + m_formatCombo = new QComboBox(this); + m_formatCombo->addItem("1280x720@30fps UYVY (Default/Supported)", "1280,720,30,UYVY"); + groupLayout->addWidget(formatLabel); + groupLayout->addWidget(m_formatCombo); + + // Pipeline presets + QLabel* presetsLabel = new QLabel("Pipeline Presets:", this); + m_pipelinePresets = new QComboBox(this); + m_pipelinePresets->addItem("MJPEG UDP Stream (Best for raw formats)", "videoconvert ! jpegenc ! rtpjpegpay ! udpsink host=127.0.0.1 port=5000"); + m_pipelinePresets->addItem("UDP H.264 Stream (Requires gst-libav)", "videoconvert ! x264enc tune=zerolatency ! rtph264pay ! udpsink host=127.0.0.1 port=5000"); + m_pipelinePresets->addItem("Custom", ""); + m_pipelinePresets->addItem("Test - Fake Sink (No Output)", "fakesink"); + m_pipelinePresets->addItem("Local Display", "videoconvert ! autovideosink"); + m_pipelinePresets->addItem("TCP H.264 Stream", "videoconvert ! x264enc ! h264parse ! mpegtsmux ! tcpserversink host=0.0.0.0 port=5000"); + m_pipelinePresets->addItem("MJPEG HTTP Stream", "videoconvert ! jpegenc ! multipartmux ! tcpserversink host=0.0.0.0 port=8080"); + m_pipelinePresets->addItem("Save to File", "videoconvert ! x264enc ! mp4mux ! filesink location=/tmp/output.mp4"); + + connect(m_pipelinePresets, QOverload::of(&QComboBox::currentIndexChanged), + this, &GStreamerPipelineWidget::onPipelinePresetChanged); + + groupLayout->addWidget(presetsLabel); + groupLayout->addWidget(m_pipelinePresets); + + // Pipeline editor + QLabel* pipelineLabel = new QLabel("Pipeline:", this); + m_pipelineEdit = new QTextEdit(this); + m_pipelineEdit->setMaximumHeight(80); + m_pipelineEdit->setPlaceholderText("Enter GStreamer pipeline here...\nExample: videoconvert ! autovideosink"); + + groupLayout->addWidget(pipelineLabel); + groupLayout->addWidget(m_pipelineEdit); + + // Set pipeline button + m_setPipelineBtn = new QPushButton("Set Pipeline", this); + connect(m_setPipelineBtn, &QPushButton::clicked, this, &GStreamerPipelineWidget::onSetPipeline); + groupLayout->addWidget(m_setPipelineBtn); + + // Stream control buttons + QHBoxLayout* buttonLayout = new QHBoxLayout(); + m_startStreamBtn = new QPushButton("Start Stream", this); + m_stopStreamBtn = new QPushButton("Stop Stream", this); + m_getStatusBtn = new QPushButton("Get Status", this); + + connect(m_startStreamBtn, &QPushButton::clicked, this, &GStreamerPipelineWidget::onStartStream); + connect(m_stopStreamBtn, &QPushButton::clicked, this, &GStreamerPipelineWidget::onStopStream); + connect(m_getStatusBtn, &QPushButton::clicked, this, &GStreamerPipelineWidget::onGetStatus); + + buttonLayout->addWidget(m_startStreamBtn); + buttonLayout->addWidget(m_stopStreamBtn); + buttonLayout->addWidget(m_getStatusBtn); + groupLayout->addLayout(buttonLayout); + + // Status label + m_statusLabel = new QLabel("Status: Unknown", this); + m_statusLabel->setStyleSheet("QLabel { background-color: #f0f0f0; padding: 5px; border-radius: 3px; }"); + groupLayout->addWidget(m_statusLabel); + + mainLayout->addWidget(groupBox); + mainLayout->addStretch(); + + setLayout(mainLayout); +} + +void GStreamerPipelineWidget::onPipelinePresetChanged(int index) +{ + QString pipeline = m_pipelinePresets->currentData().toString(); + if (!pipeline.isEmpty()) { + m_pipelineEdit->setPlainText(pipeline); + } +} + +void GStreamerPipelineWidget::onSetPipeline() +{ + QString pipeline = m_pipelineEdit->toPlainText().trimmed(); + if (pipeline.isEmpty()) { + updateStatus("Error: Empty pipeline", false); + return; + } + + QJsonObject params; + params["pipeline"] = pipeline; + + m_socketClient->sendCommand("set_pipeline", params, + [this](const QJsonObject& response) { + updateStatus("Pipeline set successfully", false); + }, + [this](const QString& error) { + updateStatus("Error: " + error, false); + }); +} + +void GStreamerPipelineWidget::onStartStream() +{ + // First ensure format is set, then start stream + QString formatData = m_formatCombo->currentData().toString(); + QStringList parts = formatData.split(','); + + QJsonObject formatParams; + formatParams["width"] = parts[0]; + formatParams["height"] = parts[1]; + formatParams["framerate"] = parts[2]; + formatParams["format"] = parts[3]; + + m_socketClient->sendCommand("set_format", formatParams, + [this](const QJsonObject& response) { + // Now start stream + m_socketClient->sendCommand("start_stream", QJsonObject(), + [this](const QJsonObject& response) { + updateStatus("Streaming started", true); + }, + [this](const QString& error) { + updateStatus("Error: Failed to start stream: " + error, false); + }); + }, + [this](const QString& error) { + // Format setting failed, but maybe it was already set - try starting anyway + m_socketClient->sendCommand("start_stream", QJsonObject(), + [this](const QJsonObject& response) { + updateStatus("Streaming started", true); + }, + [this](const QString& error) { + updateStatus("Error: Failed to start stream: " + error, false); + }); + }); +} + +void GStreamerPipelineWidget::onStopStream() +{ + m_socketClient->sendCommand("stop_stream", QJsonObject(), + [this](const QJsonObject& response) { + updateStatus("Streaming stopped", false); + }, + [this](const QString& error) { + updateStatus("Error: Failed to stop stream: " + error, false); + }); +} + +void GStreamerPipelineWidget::onGetStatus() +{ + m_socketClient->sendCommand("get_status", QJsonObject(), + [this](const QJsonObject& response) { + bool streaming = response["streaming"].toBool(); + QString pipeline = response["pipeline"].toString(); + + updateStatus(streaming ? "Streaming" : "Stopped", streaming); + + if (!pipeline.isEmpty() && m_pipelineEdit->toPlainText().isEmpty()) { + m_pipelineEdit->setPlainText(pipeline); + } + }, + [this](const QString& error) { + updateStatus("Connection Error", false); + }); +} + +void GStreamerPipelineWidget::updateStatus(const QString& status, bool streaming) +{ + m_statusLabel->setText("Status: " + status); + + if (streaming) { + m_statusLabel->setStyleSheet("QLabel { background-color: #90EE90; padding: 5px; border-radius: 3px; }"); + } else if (status.contains("Error")) { + m_statusLabel->setStyleSheet("QLabel { background-color: #FFB6C1; padding: 5px; border-radius: 3px; }"); + } else { + m_statusLabel->setStyleSheet("QLabel { background-color: #f0f0f0; padding: 5px; border-radius: 3px; }"); + } +} + +void GStreamerPipelineWidget::onQuickStart() +{ + // Disable button during process + m_quickStartBtn->setEnabled(false); + m_quickStartBtn->setText("Configuring..."); + + // Step 1: Set format + QString formatData = m_formatCombo->currentData().toString(); + QStringList parts = formatData.split(','); + + QJsonObject formatParams; + formatParams["width"] = parts[0]; + formatParams["height"] = parts[1]; + formatParams["framerate"] = parts[2]; + formatParams["format"] = parts[3]; + + m_socketClient->sendCommand("set_format", formatParams, + [this](const QJsonObject& response) { + // Step 2: Use selected preset pipeline or default to MJPEG + QString pipeline = m_pipelinePresets->currentData().toString(); + if (pipeline.isEmpty()) { + pipeline = "videoconvert ! jpegenc ! rtpjpegpay ! udpsink host=127.0.0.1 port=5000"; + } + m_pipelineEdit->setPlainText(pipeline); + + QJsonObject pipelineParams; + pipelineParams["pipeline"] = pipeline; + + m_socketClient->sendCommand("set_pipeline", pipelineParams, + [this](const QJsonObject& response) { + // Step 3: Start stream + m_socketClient->sendCommand("start_stream", QJsonObject(), + [this](const QJsonObject& response) { + updateStatus("Streaming started - Switch to Video Viewer tab and click 'Start Viewer'", true); + m_quickStartBtn->setEnabled(true); + m_quickStartBtn->setText("⚡ Quick Start (Auto Configure & Stream)"); + }, + [this](const QString& error) { + m_quickStartBtn->setEnabled(true); + m_quickStartBtn->setText("⚡ Quick Start (Auto Configure & Stream)"); + updateStatus("Error: Failed to start stream: " + error, false); + }); + }, + [this](const QString& error) { + m_quickStartBtn->setEnabled(true); + m_quickStartBtn->setText("⚡ Quick Start (Auto Configure & Stream)"); + updateStatus("Error: Failed to set pipeline: " + error, false); + }); + }, + [this](const QString& error) { + m_quickStartBtn->setEnabled(true); + m_quickStartBtn->setText("⚡ Quick Start (Auto Configure & Stream)"); + updateStatus("Error: Failed to set format: " + error, false); + }); +} + +void GStreamerPipelineWidget::setFormatAndPipeline() +{ + // Called before manual stream start to ensure format is set + QString formatData = m_formatCombo->currentData().toString(); + QStringList parts = formatData.split(','); + + QJsonObject formatParams; + formatParams["width"] = parts[0]; + formatParams["height"] = parts[1]; + formatParams["framerate"] = parts[2]; + formatParams["format"] = parts[3]; + + m_socketClient->sendCommand("set_format", formatParams, + [](const QJsonObject& response) {}, + [](const QString& error) {}); +} + +void GStreamerPipelineWidget::fetchAvailableFormats() +{ + m_socketClient->sendCommand("get_formats", QJsonObject(), + [this](const QJsonObject& response) { + onFormatsReceived(response); + }, + [this](const QString& error) { + qDebug() << "Failed to fetch formats:" << error; + }); +} + +void GStreamerPipelineWidget::onFormatsReceived(const QJsonObject& response) +{ + if (!response.contains("formats")) { + return; + } + + QJsonArray formats = response["formats"].toArray(); + if (formats.isEmpty()) { + return; + } + + // Clear existing formats + m_formatCombo->clear(); + + // Add all available formats + for (const QJsonValue& val : formats) { + QJsonObject fmt = val.toObject(); + int width = fmt["width"].toInt(); + int height = fmt["height"].toInt(); + int fps = fmt["framerate"].toInt(); + QString format = fmt["format"].toString(); + + QString displayText = QString("%1x%2@%3fps %4") + .arg(width).arg(height).arg(fps).arg(format); + QString data = QString("%1,%2,%3,%4").arg(width).arg(height).arg(fps).arg(format); + + m_formatCombo->addItem(displayText, data); + } + + qDebug() << "Loaded" << formats.size() << "available formats from camera"; +} diff --git a/gstreamerpipelinewidget.h b/gstreamerpipelinewidget.h new file mode 100644 index 0000000..baba72d --- /dev/null +++ b/gstreamerpipelinewidget.h @@ -0,0 +1,46 @@ +#ifndef GSTREAMERPIPELINEWIDGET_H +#define GSTREAMERPIPELINEWIDGET_H + +#include +#include +#include +#include +#include +#include "socketclient.h" + +class GStreamerPipelineWidget : public QWidget +{ + Q_OBJECT + +public: + explicit GStreamerPipelineWidget(SocketClient* socketClient, QWidget *parent = nullptr); + +private slots: + void onSetPipeline(); + void onStartStream(); + void onStopStream(); + void onGetStatus(); + void onPipelinePresetChanged(int index); + void onQuickStart(); + void onFormatsReceived(const QJsonObject& response); + +private: + void setupUI(); + void updateStatus(const QString& status, bool streaming); + void setFormatAndPipeline(); + void fetchAvailableFormats(); + + SocketClient* m_socketClient; + QTextEdit* m_pipelineEdit; + QPushButton* m_setPipelineBtn; + QPushButton* m_startStreamBtn; + QPushButton* m_stopStreamBtn; + QPushButton* m_getStatusBtn; + QPushButton* m_quickStartBtn; + QLabel* m_statusLabel; + QLabel* m_infoLabel; + QComboBox* m_pipelinePresets; + QComboBox* m_formatCombo; +}; + +#endif // GSTREAMERPIPELINEWIDGET_H diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..fd3e533 --- /dev/null +++ b/main.cpp @@ -0,0 +1,11 @@ +#include "mainwindow.h" + +#include + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + MainWindow w; + w.show(); + return a.exec(); +} diff --git a/mainwindow.cpp b/mainwindow.cpp new file mode 100644 index 0000000..56ed8c3 --- /dev/null +++ b/mainwindow.cpp @@ -0,0 +1,49 @@ +#include "mainwindow.h" +#include "ui_mainwindow.h" +#include +#include +#include +#include + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , ui(new Ui::MainWindow) +{ + ui->setupUi(this); + setWindowTitle("GStreamer Camera Viewer"); + resize(1400, 900); + + setupUI(); +} + +MainWindow::~MainWindow() +{ + delete m_socketClient; + delete ui; +} + +void MainWindow::setupUI() +{ + // Create socket client + m_socketClient = new SocketClient("/tmp/vizion_control.sock", this); + + // Create widgets + m_videoWidget = new VideoViewerWidget(this); + m_pipelineWidget = new GStreamerPipelineWidget(m_socketClient, this); + m_cameraWidget = new CameraControlWidget(m_socketClient, this); + + // Create tab widget for controls + QTabWidget* controlTabs = new QTabWidget(this); + controlTabs->addTab(m_pipelineWidget, "Pipeline Control"); + controlTabs->addTab(m_cameraWidget, "Camera Control"); + + // Create vertical splitter: video on top, controls on bottom + QSplitter* mainSplitter = new QSplitter(Qt::Vertical, this); + mainSplitter->addWidget(m_videoWidget); + mainSplitter->addWidget(controlTabs); + mainSplitter->setStretchFactor(0, 3); // Video gets more space + mainSplitter->setStretchFactor(1, 1); // Controls get less space + + // Set as central widget + setCentralWidget(mainSplitter); +} diff --git a/mainwindow.h b/mainwindow.h new file mode 100644 index 0000000..4c698e4 --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,33 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include "socketclient.h" +#include "gstreamerpipelinewidget.h" +#include "cameracontrolwidget.h" +#include "videoviewerwidget.h" + +QT_BEGIN_NAMESPACE +namespace Ui { +class MainWindow; +} +QT_END_NAMESPACE + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + +private: + void setupUI(); + + Ui::MainWindow *ui; + SocketClient* m_socketClient; + GStreamerPipelineWidget* m_pipelineWidget; + CameraControlWidget* m_cameraWidget; + VideoViewerWidget* m_videoWidget; +}; +#endif // MAINWINDOW_H diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..c6854ca --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,31 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + MainWindow + + + + + + 0 + 0 + 800 + 23 + + + + + + + + diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..7f17f02 --- /dev/null +++ b/run.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Run script for gstreamerViewer + +SOCKET="/tmp/vizion_control.sock" +EXECUTABLE="./build/gstreamerViewer" + +# Check if executable exists +if [ ! -f "$EXECUTABLE" ]; then + echo "Error: Executable not found at $EXECUTABLE" + echo "Please run ./build.sh first to build the application." + exit 1 +fi + +# Check if VizionStreamer socket exists +if [ ! -S "$SOCKET" ]; then + echo "Warning: VizionStreamer socket not found at $SOCKET" + echo "Please ensure VizionStreamer backend is running." + echo "" + read -p "Continue anyway? (y/n) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi +fi + +echo "Starting gstreamerViewer..." +cd build +./gstreamerViewer diff --git a/socketclient.cpp b/socketclient.cpp new file mode 100644 index 0000000..69a89db --- /dev/null +++ b/socketclient.cpp @@ -0,0 +1,111 @@ +#include "socketclient.h" +#include +#include +#include +#include +#include +#include +#include +#include + +SocketClient::SocketClient(const QString& socketPath, QObject *parent) + : QObject(parent), m_socketPath(socketPath) +{ +} + +void SocketClient::sendCommand(const QString& command, const QJsonObject& params, + ResponseCallback onSuccess, ErrorCallback onError) +{ + qDebug() << "[SocketClient] Sending command:" << command; + qDebug() << "[SocketClient] Parameters:" << params; + + QJsonObject response = executeCommand(command, params); + + qDebug() << "[SocketClient] Response:" << response; + + if (response.isEmpty()) { + QString errorMsg = "Failed to connect to socket: " + m_socketPath; + qDebug() << "[SocketClient] ERROR:" << errorMsg; + emit connectionError(errorMsg); + if (onError) { + onError(errorMsg); + } + return; + } + + QString status = response["status"].toString(); + if (status == "success") { + qDebug() << "[SocketClient] Command successful"; + emit commandSuccess(response); + if (onSuccess) { + onSuccess(response); + } + } else { + QString errorMsg = response["message"].toString("Unknown error"); + qDebug() << "[SocketClient] Command error:" << errorMsg; + emit commandError(errorMsg); + if (onError) { + onError(errorMsg); + } + } +} + +QJsonObject SocketClient::executeCommand(const QString& command, const QJsonObject& params) +{ + int sock = socket(AF_UNIX, SOCK_STREAM, 0); + if (sock < 0) { + qDebug() << "[SocketClient] Failed to create socket"; + return QJsonObject(); + } + + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, m_socketPath.toUtf8().constData(), sizeof(addr.sun_path) - 1); + + if (::connect(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + qDebug() << "[SocketClient] Failed to connect to socket:" << m_socketPath; + qDebug() << "[SocketClient] Error:" << strerror(errno); + close(sock); + return QJsonObject(); + } + + QJsonObject cmdObj; + cmdObj["command"] = command; + if (!params.isEmpty()) { + cmdObj["params"] = params; + } + + QJsonDocument cmdDoc(cmdObj); + QByteArray cmdData = cmdDoc.toJson(QJsonDocument::Compact); + + qDebug() << "[SocketClient] Sending:" << cmdData; + + ssize_t sent = send(sock, cmdData.constData(), cmdData.size(), 0); + if (sent < 0) { + qDebug() << "[SocketClient] Failed to send data:" << strerror(errno); + close(sock); + return QJsonObject(); + } + + char buffer[4096]; + int bytesRead = recv(sock, buffer, sizeof(buffer) - 1, 0); + + if (bytesRead < 0) { + qDebug() << "[SocketClient] Failed to receive data:" << strerror(errno); + close(sock); + return QJsonObject(); + } + + close(sock); + + if (bytesRead > 0) { + buffer[bytesRead] = '\0'; + qDebug() << "[SocketClient] Received:" << QByteArray(buffer, bytesRead); + QJsonDocument responseDoc = QJsonDocument::fromJson(QByteArray(buffer, bytesRead)); + return responseDoc.object(); + } + + qDebug() << "[SocketClient] No data received"; + return QJsonObject(); +} diff --git a/socketclient.h b/socketclient.h new file mode 100644 index 0000000..c28b9db --- /dev/null +++ b/socketclient.h @@ -0,0 +1,33 @@ +#ifndef SOCKETCLIENT_H +#define SOCKETCLIENT_H + +#include +#include +#include +#include +#include + +class SocketClient : public QObject +{ + Q_OBJECT + +public: + explicit SocketClient(const QString& socketPath = "/tmp/vizion_control.sock", QObject *parent = nullptr); + + using ResponseCallback = std::function; + using ErrorCallback = std::function; + + void sendCommand(const QString& command, const QJsonObject& params = QJsonObject(), + ResponseCallback onSuccess = nullptr, ErrorCallback onError = nullptr); + +signals: + void commandSuccess(const QJsonObject& response); + void commandError(const QString& errorMessage); + void connectionError(const QString& errorMessage); + +private: + QString m_socketPath; + QJsonObject executeCommand(const QString& command, const QJsonObject& params); +}; + +#endif // SOCKETCLIENT_H diff --git a/test_connection.sh b/test_connection.sh new file mode 100755 index 0000000..18b01b9 --- /dev/null +++ b/test_connection.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# Test script to verify VizionStreamer connection + +SOCKET="/tmp/vizion_control.sock" + +echo "=== VizionStreamer Connection Test ===" +echo "" + +# Check if socket exists +if [ ! -S "$SOCKET" ]; then + echo "❌ FAIL: Socket not found at $SOCKET" + echo "Please ensure VizionStreamer backend is running." + exit 1 +fi + +echo "✓ Socket found at $SOCKET" +echo "" + +# Test get_status command +echo "Testing get_status command..." +RESPONSE=$(echo '{"command":"get_status"}' | socat - UNIX-CONNECT:$SOCKET 2>/dev/null) + +if [ $? -eq 0 ]; then + echo "✓ Connection successful" + echo "Response: $RESPONSE" + echo "" +else + echo "❌ FAIL: Could not connect to socket" + exit 1 +fi + +# Test get_formats command +echo "Testing get_formats command..." +FORMATS=$(echo '{"command":"get_formats"}' | socat - UNIX-CONNECT:$SOCKET 2>/dev/null) + +if [ $? -eq 0 ]; then + echo "✓ get_formats successful" + + # Pretty print if python3 is available + if command -v python3 &> /dev/null; then + echo "$FORMATS" | python3 -m json.tool 2>/dev/null || echo "$FORMATS" + else + echo "$FORMATS" + fi + echo "" +else + echo "❌ FAIL: Could not get formats" + exit 1 +fi + +echo "=== All tests passed! ===" +echo "VizionStreamer backend is ready for use." diff --git a/videoviewerwidget.cpp b/videoviewerwidget.cpp new file mode 100644 index 0000000..b686cc9 --- /dev/null +++ b/videoviewerwidget.cpp @@ -0,0 +1,323 @@ +#include "videoviewerwidget.h" +#include +#include +#include +#include +#include +#include +#include + +VideoViewerWidget::VideoViewerWidget(QWidget *parent) + : QWidget(parent), m_pipeline(nullptr), m_videoSink(nullptr), + m_busWatchId(0), m_windowId(0) +{ + initGStreamer(); + setupUI(); +} + +VideoViewerWidget::~VideoViewerWidget() +{ + cleanupGStreamer(); +} + +void VideoViewerWidget::initGStreamer() +{ + gst_init(nullptr, nullptr); +} + +void VideoViewerWidget::setupUI() +{ + QVBoxLayout* mainLayout = new QVBoxLayout(this); + + // Video display container + QGroupBox* videoGroup = new QGroupBox("Video Display", this); + QVBoxLayout* videoLayout = new QVBoxLayout(); + + m_videoContainer = new QWidget(this); + m_videoContainer->setMinimumSize(640, 480); + m_videoContainer->setStyleSheet("background-color: black;"); + m_videoContainer->setAttribute(Qt::WA_NativeWindow); + + videoLayout->addWidget(m_videoContainer); + videoGroup->setLayout(videoLayout); + + // Controls + QGroupBox* controlGroup = new QGroupBox("Viewer Controls", this); + QVBoxLayout* controlLayout = new QVBoxLayout(); + + // Source type selection + QHBoxLayout* sourceLayout = new QHBoxLayout(); + sourceLayout->addWidget(new QLabel("Source Type:", this)); + m_sourceType = new QComboBox(this); + m_sourceType->addItem("UDP MJPEG Stream (No plugins needed)", "udp-mjpeg"); + m_sourceType->addItem("UDP H.264 Stream (Requires gst-libav)", "udp-h264"); + m_sourceType->addItem("TCP H.264 Stream", "tcp"); + m_sourceType->addItem("MJPEG HTTP Stream", "http"); + m_sourceType->addItem("Test Pattern", "test"); + connect(m_sourceType, QOverload::of(&QComboBox::currentIndexChanged), + this, &VideoViewerWidget::onSourceTypeChanged); + sourceLayout->addWidget(m_sourceType); + + // Host and port + QFormLayout* formLayout = new QFormLayout(); + m_hostEdit = new QLineEdit("127.0.0.1", this); + m_portEdit = new QLineEdit("5000", this); + formLayout->addRow("Host:", m_hostEdit); + formLayout->addRow("Port:", m_portEdit); + + // Control buttons + QHBoxLayout* buttonLayout = new QHBoxLayout(); + m_startBtn = new QPushButton("Start Viewer", this); + m_stopBtn = new QPushButton("Stop Viewer", this); + m_stopBtn->setEnabled(false); + + connect(m_startBtn, &QPushButton::clicked, this, &VideoViewerWidget::onStartViewer); + connect(m_stopBtn, &QPushButton::clicked, this, &VideoViewerWidget::onStopViewer); + + buttonLayout->addWidget(m_startBtn); + buttonLayout->addWidget(m_stopBtn); + + // Status label + m_statusLabel = new QLabel("Status: Stopped", this); + m_statusLabel->setStyleSheet("QLabel { background-color: #f0f0f0; padding: 5px; border-radius: 3px; }"); + + controlLayout->addLayout(sourceLayout); + controlLayout->addLayout(formLayout); + controlLayout->addLayout(buttonLayout); + controlLayout->addWidget(m_statusLabel); + controlGroup->setLayout(controlLayout); + + mainLayout->addWidget(videoGroup, 1); + mainLayout->addWidget(controlGroup); + + setLayout(mainLayout); +} + +void VideoViewerWidget::showEvent(QShowEvent* event) +{ + QWidget::showEvent(event); + if (!m_windowId) { + m_videoContainer->winId(); // Force window creation + QTimer::singleShot(100, this, [this]() { + m_windowId = m_videoContainer->winId(); + qDebug() << "[VideoViewer] Window ID initialized:" << m_windowId; + }); + } +} + +QString VideoViewerWidget::buildPipelineString() +{ + QString sourceType = m_sourceType->currentData().toString(); + QString host = m_hostEdit->text(); + QString port = m_portEdit->text(); + QString pipeline; + + // Note: Using autovideosink which opens a separate window + // VideoOverlay with Qt widgets doesn't work reliably on this system + if (sourceType == "udp-mjpeg") { + pipeline = QString("udpsrc port=%1 ! application/x-rtp,encoding-name=JPEG,payload=26 ! " + "rtpjpegdepay ! jpegdec ! autovideosink") + .arg(port); + } else if (sourceType == "udp-h264") { + pipeline = QString("udpsrc port=%1 ! application/x-rtp,encoding-name=H264 ! " + "rtph264depay ! h264parse ! avdec_h264 ! videoconvert ! autovideosink") + .arg(port); + } else if (sourceType == "tcp") { + pipeline = QString("tcpclientsrc host=%1 port=%2 ! tsdemux ! h264parse ! avdec_h264 ! " + "videoconvert ! autovideosink") + .arg(host).arg(port); + } else if (sourceType == "http") { + pipeline = QString("souphttpsrc location=http://%1:%2 ! multipartdemux ! jpegdec ! " + "videoconvert ! autovideosink") + .arg(host).arg(port); + } else if (sourceType == "test") { + pipeline = "videotestsrc ! autovideosink"; + } + + return pipeline; +} + +void VideoViewerWidget::startPipeline() +{ + if (m_pipeline) { + stopPipeline(); + } + + QString pipelineStr = buildPipelineString(); + qDebug() << "[VideoViewer] Starting pipeline:" << pipelineStr; + + GError* error = nullptr; + + m_pipeline = gst_parse_launch(pipelineStr.toUtf8().constData(), &error); + + if (error) { + m_statusLabel->setText(QString("Status: Pipeline Error - %1").arg(error->message)); + m_statusLabel->setStyleSheet("QLabel { background-color: #FFB6C1; padding: 5px; border-radius: 3px; }"); + g_error_free(error); + return; + } + + if (!m_pipeline) { + m_statusLabel->setText("Status: Failed to create pipeline"); + m_statusLabel->setStyleSheet("QLabel { background-color: #FFB6C1; padding: 5px; border-radius: 3px; }"); + return; + } + + // Set up bus callback + GstBus* bus = gst_element_get_bus(m_pipeline); + m_busWatchId = gst_bus_add_watch(bus, busCallback, this); + gst_object_unref(bus); + + // Note: VideoOverlay disabled - using autovideosink with separate window instead + + // Start playing + GstStateChangeReturn ret = gst_element_set_state(m_pipeline, GST_STATE_PLAYING); + + qDebug() << "[VideoViewer] Pipeline state change return:" << ret; + + if (ret == GST_STATE_CHANGE_FAILURE) { + m_statusLabel->setText("Status: Failed to start pipeline"); + m_statusLabel->setStyleSheet("QLabel { background-color: #FFB6C1; padding: 5px; border-radius: 3px; }"); + cleanupGStreamer(); + return; + } + + qDebug() << "[VideoViewer] Pipeline started successfully"; + m_statusLabel->setText("Status: Playing"); + m_statusLabel->setStyleSheet("QLabel { background-color: #90EE90; padding: 5px; border-radius: 3px; }"); + m_startBtn->setEnabled(false); + m_stopBtn->setEnabled(true); +} + +void VideoViewerWidget::stopPipeline() +{ + if (m_pipeline) { + gst_element_set_state(m_pipeline, GST_STATE_NULL); + gst_object_unref(m_pipeline); + m_pipeline = nullptr; + } + + if (m_videoSink) { + gst_object_unref(m_videoSink); + m_videoSink = nullptr; + } + + if (m_busWatchId > 0) { + g_source_remove(m_busWatchId); + m_busWatchId = 0; + } + + m_statusLabel->setText("Status: Stopped"); + m_statusLabel->setStyleSheet("QLabel { background-color: #f0f0f0; padding: 5px; border-radius: 3px; }"); + m_startBtn->setEnabled(true); + m_stopBtn->setEnabled(false); +} + +void VideoViewerWidget::cleanupGStreamer() +{ + stopPipeline(); +} + +gboolean VideoViewerWidget::busCallback(GstBus* bus, GstMessage* msg, gpointer data) +{ + VideoViewerWidget* viewer = static_cast(data); + + switch (GST_MESSAGE_TYPE(msg)) { + case GST_MESSAGE_ERROR: { + GError* err; + gchar* debug_info; + gst_message_parse_error(msg, &err, &debug_info); + + QString errorMsg = QString("GStreamer Error: %1\nDebug: %2") + .arg(err->message) + .arg(debug_info ? debug_info : "none"); + + qDebug() << "[VideoViewer] ERROR:" << errorMsg; + + QMetaObject::invokeMethod(viewer, [viewer, errorMsg]() { + viewer->m_statusLabel->setText("Status: Stream Error - " + errorMsg); + viewer->m_statusLabel->setStyleSheet("QLabel { background-color: #FFB6C1; padding: 5px; border-radius: 3px; }"); + viewer->stopPipeline(); + }, Qt::QueuedConnection); + + g_error_free(err); + g_free(debug_info); + break; + } + case GST_MESSAGE_EOS: + qDebug() << "[VideoViewer] End of stream"; + QMetaObject::invokeMethod(viewer, [viewer]() { + viewer->m_statusLabel->setText("Status: End of Stream"); + viewer->stopPipeline(); + }, Qt::QueuedConnection); + break; + case GST_MESSAGE_STATE_CHANGED: + if (GST_MESSAGE_SRC(msg) == GST_OBJECT(viewer->m_pipeline)) { + GstState oldState, newState, pendingState; + gst_message_parse_state_changed(msg, &oldState, &newState, &pendingState); + qDebug() << "[VideoViewer] State changed:" + << gst_element_state_get_name(oldState) << "->" + << gst_element_state_get_name(newState); + } + break; + case GST_MESSAGE_WARNING: { + GError* err; + gchar* debug_info; + gst_message_parse_warning(msg, &err, &debug_info); + qDebug() << "[VideoViewer] WARNING:" << err->message; + g_error_free(err); + g_free(debug_info); + break; + } + case GST_MESSAGE_INFO: { + GError* err; + gchar* debug_info; + gst_message_parse_info(msg, &err, &debug_info); + qDebug() << "[VideoViewer] INFO:" << err->message; + g_error_free(err); + g_free(debug_info); + break; + } + default: + break; + } + + return TRUE; +} + +void VideoViewerWidget::onStartViewer() +{ + startPipeline(); +} + +void VideoViewerWidget::onStopViewer() +{ + stopPipeline(); +} + +void VideoViewerWidget::onSourceTypeChanged(int index) +{ + QString sourceType = m_sourceType->currentData().toString(); + + bool needsNetwork = (sourceType != "test"); + bool isUdp = (sourceType == "udp-mjpeg" || sourceType == "udp-h264"); + m_hostEdit->setEnabled(needsNetwork && !isUdp); + m_portEdit->setEnabled(needsNetwork); +} + +void VideoViewerWidget::onPrepareWindowHandle(GstBus* bus, GstMessage* msg, gpointer data) +{ + if (!gst_is_video_overlay_prepare_window_handle_message(msg)) { + return; + } + + VideoViewerWidget* viewer = static_cast(data); + + if (viewer->m_windowId) { + GstElement* sink = GST_ELEMENT(GST_MESSAGE_SRC(msg)); + qDebug() << "[VideoViewer] prepare-window-handle: Setting window ID" << viewer->m_windowId; + gst_video_overlay_set_window_handle(GST_VIDEO_OVERLAY(sink), viewer->m_windowId); + } else { + qDebug() << "[VideoViewer] prepare-window-handle: No window ID available yet"; + } +} diff --git a/videoviewerwidget.h b/videoviewerwidget.h new file mode 100644 index 0000000..ddd45d4 --- /dev/null +++ b/videoviewerwidget.h @@ -0,0 +1,55 @@ +#ifndef VIDEOVIEWERWIDGET_H +#define VIDEOVIEWERWIDGET_H + +#include +#include +#include +#include +#include +#include + +class VideoViewerWidget : public QWidget +{ + Q_OBJECT + +public: + explicit VideoViewerWidget(QWidget *parent = nullptr); + ~VideoViewerWidget(); + +protected: + void showEvent(QShowEvent* event) override; + +private slots: + void onStartViewer(); + void onStopViewer(); + void onSourceTypeChanged(int index); + +private: + void setupUI(); + void initGStreamer(); + void cleanupGStreamer(); + void startPipeline(); + void stopPipeline(); + QString buildPipelineString(); + void setupVideoOverlay(); + + static gboolean busCallback(GstBus* bus, GstMessage* msg, gpointer data); + static void onPrepareWindowHandle(GstBus* bus, GstMessage* msg, gpointer data); + + // UI elements + QWidget* m_videoContainer; + QPushButton* m_startBtn; + QPushButton* m_stopBtn; + QComboBox* m_sourceType; + QLineEdit* m_hostEdit; + QLineEdit* m_portEdit; + QLabel* m_statusLabel; + + // GStreamer elements + GstElement* m_pipeline; + GstElement* m_videoSink; + guint m_busWatchId; + WId m_windowId; +}; + +#endif // VIDEOVIEWERWIDGET_H