first commit
This commit is contained in:
53
.gitignore
vendored
Normal file
53
.gitignore
vendored
Normal file
@@ -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/
|
||||
47
CMakeLists.txt
Normal file
47
CMakeLists.txt
Normal file
@@ -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})
|
||||
347
README.md
Normal file
347
README.md
Normal file
@@ -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`
|
||||
663
SOCKET_API.md
Normal file
663
SOCKET_API.md
Normal file
@@ -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 <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
|
||||
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)
|
||||
28
build.sh
Executable file
28
build.sh
Executable file
@@ -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 ""
|
||||
381
cameracontrolwidget.cpp
Normal file
381
cameracontrolwidget.cpp
Normal file
@@ -0,0 +1,381 @@
|
||||
#include "cameracontrolwidget.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QFormLayout>
|
||||
#include <QScrollArea>
|
||||
#include <QButtonGroup>
|
||||
#include <QJsonArray>
|
||||
|
||||
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<int>::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; }");
|
||||
}
|
||||
}
|
||||
88
cameracontrolwidget.h
Normal file
88
cameracontrolwidget.h
Normal file
@@ -0,0 +1,88 @@
|
||||
#ifndef CAMERACONTROLWIDGET_H
|
||||
#define CAMERACONTROLWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QSlider>
|
||||
#include <QSpinBox>
|
||||
#include <QComboBox>
|
||||
#include <QRadioButton>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QGroupBox>
|
||||
#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
|
||||
328
gstreamerpipelinewidget.cpp
Normal file
328
gstreamerpipelinewidget.cpp
Normal file
@@ -0,0 +1,328 @@
|
||||
#include "gstreamerpipelinewidget.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QFrame>
|
||||
#include <QTimer>
|
||||
#include <QJsonArray>
|
||||
|
||||
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(
|
||||
"<b>Quick Start:</b> Click 'Quick Start' to automatically configure and start streaming.<br>"
|
||||
"<b>Manual:</b> 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<int>::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";
|
||||
}
|
||||
46
gstreamerpipelinewidget.h
Normal file
46
gstreamerpipelinewidget.h
Normal file
@@ -0,0 +1,46 @@
|
||||
#ifndef GSTREAMERPIPELINEWIDGET_H
|
||||
#define GSTREAMERPIPELINEWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QComboBox>
|
||||
#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
|
||||
11
main.cpp
Normal file
11
main.cpp
Normal file
@@ -0,0 +1,11 @@
|
||||
#include "mainwindow.h"
|
||||
|
||||
#include <QApplication>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication a(argc, argv);
|
||||
MainWindow w;
|
||||
w.show();
|
||||
return a.exec();
|
||||
}
|
||||
49
mainwindow.cpp
Normal file
49
mainwindow.cpp
Normal file
@@ -0,0 +1,49 @@
|
||||
#include "mainwindow.h"
|
||||
#include "ui_mainwindow.h"
|
||||
#include <QHBoxLayout>
|
||||
#include <QVBoxLayout>
|
||||
#include <QSplitter>
|
||||
#include <QTabWidget>
|
||||
|
||||
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);
|
||||
}
|
||||
33
mainwindow.h
Normal file
33
mainwindow.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#ifndef MAINWINDOW_H
|
||||
#define MAINWINDOW_H
|
||||
|
||||
#include <QMainWindow>
|
||||
#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
|
||||
31
mainwindow.ui
Normal file
31
mainwindow.ui
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>600</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>MainWindow</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget"/>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>23</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusbar"/>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
28
run.sh
Executable file
28
run.sh
Executable file
@@ -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
|
||||
111
socketclient.cpp
Normal file
111
socketclient.cpp
Normal file
@@ -0,0 +1,111 @@
|
||||
#include "socketclient.h"
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
#include <cstring>
|
||||
#include <errno.h>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QDebug>
|
||||
|
||||
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();
|
||||
}
|
||||
33
socketclient.h
Normal file
33
socketclient.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#ifndef SOCKETCLIENT_H
|
||||
#define SOCKETCLIENT_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <functional>
|
||||
|
||||
class SocketClient : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SocketClient(const QString& socketPath = "/tmp/vizion_control.sock", QObject *parent = nullptr);
|
||||
|
||||
using ResponseCallback = std::function<void(const QJsonObject&)>;
|
||||
using ErrorCallback = std::function<void(const QString&)>;
|
||||
|
||||
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
|
||||
52
test_connection.sh
Executable file
52
test_connection.sh
Executable file
@@ -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."
|
||||
323
videoviewerwidget.cpp
Normal file
323
videoviewerwidget.cpp
Normal file
@@ -0,0 +1,323 @@
|
||||
#include "videoviewerwidget.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QFormLayout>
|
||||
#include <QGroupBox>
|
||||
#include <QDebug>
|
||||
#include <QTimer>
|
||||
#include <gst/video/videooverlay.h>
|
||||
|
||||
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<int>::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<VideoViewerWidget*>(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<VideoViewerWidget*>(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";
|
||||
}
|
||||
}
|
||||
55
videoviewerwidget.h
Normal file
55
videoviewerwidget.h
Normal file
@@ -0,0 +1,55 @@
|
||||
#ifndef VIDEOVIEWERWIDGET_H
|
||||
#define VIDEOVIEWERWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QPushButton>
|
||||
#include <QComboBox>
|
||||
#include <QLineEdit>
|
||||
#include <QLabel>
|
||||
#include <gst/gst.h>
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user