small corrections + poc004 webcam screenshot/webcam register
This commit is contained in:
@@ -5,11 +5,12 @@ members = [
|
|||||||
"poc001",
|
"poc001",
|
||||||
"poc002",
|
"poc002",
|
||||||
"poc003",
|
"poc003",
|
||||||
|
"poc004",
|
||||||
]
|
]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/poc-qt"
|
repository = "https://git.sasedev.com/Sasedev/poc-qt"
|
||||||
|
|||||||
@@ -269,7 +269,6 @@ fn main() {
|
|||||||
println!("cargo:rerun-if-changed=build.rs");
|
println!("cargo:rerun-if-changed=build.rs");
|
||||||
println!("cargo:rerun-if-changed=build_config.default.json");
|
println!("cargo:rerun-if-changed=build_config.default.json");
|
||||||
println!("cargo:rerun-if-changed=build_config.schema.json");
|
println!("cargo:rerun-if-changed=build_config.schema.json");
|
||||||
println!("cargo:rerun-if-changed=src/bridge.rs");
|
|
||||||
println!("cargo:rerun-if-changed=qml/main.qml");
|
println!("cargo:rerun-if-changed=qml/main.qml");
|
||||||
|
|
||||||
let config_path = PathBuf::from("build_config.json");
|
let config_path = PathBuf::from("build_config.json");
|
||||||
|
|||||||
@@ -269,7 +269,6 @@ fn main() {
|
|||||||
println!("cargo:rerun-if-changed=build.rs");
|
println!("cargo:rerun-if-changed=build.rs");
|
||||||
println!("cargo:rerun-if-changed=build_config.default.json");
|
println!("cargo:rerun-if-changed=build_config.default.json");
|
||||||
println!("cargo:rerun-if-changed=build_config.schema.json");
|
println!("cargo:rerun-if-changed=build_config.schema.json");
|
||||||
println!("cargo:rerun-if-changed=src/bridge.rs");
|
|
||||||
println!("cargo:rerun-if-changed=qml/main.qml");
|
println!("cargo:rerun-if-changed=qml/main.qml");
|
||||||
|
|
||||||
let config_path = PathBuf::from("build_config.json");
|
let config_path = PathBuf::from("build_config.json");
|
||||||
|
|||||||
20
poc004/Cargo.toml
Normal file
20
poc004/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# file: poc-qt/poc004/Cargo.toml
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "poc-qt004"
|
||||||
|
version = "0.4.0"
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
cxx-qt-build = { workspace = true, features = ["link_qt_object_files"] }
|
||||||
|
jsonschema = { workspace = true, features = [] }
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
serde_json = { workspace = true, features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
cxx = { workspace = true, features = [] }
|
||||||
|
cxx-qt = { workspace = true, features = [] }
|
||||||
|
cxx-qt-lib = { workspace = true, features = ["full", "link_qt_object_files"] }
|
||||||
|
|
||||||
296
poc004/build.rs
Normal file
296
poc004/build.rs
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
// file: poc-qt/poc004/build.rs
|
||||||
|
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use cxx_qt_build::{CxxQtBuilder, QmlModule};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG_STR: &str = include_str!("build_config.default.json");
|
||||||
|
const CONFIG_SCHEMA_STR: &str = include_str!("../build_config.schema.json");
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct BuildConfig {
|
||||||
|
active_profile: String,
|
||||||
|
profiles: BTreeMap<String, BuildProfile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct BuildProfile {
|
||||||
|
qt: QtConfig,
|
||||||
|
link: LinkConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct QtConfig {
|
||||||
|
qmake: String,
|
||||||
|
lib_dir: String,
|
||||||
|
qml_dir: String,
|
||||||
|
plugins_dir: String,
|
||||||
|
version_major: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct LinkConfig {
|
||||||
|
rpaths: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_user_config_exists(config_path: &Path) {
|
||||||
|
if config_path.exists() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match fs::write(config_path, DEFAULT_CONFIG_STR) {
|
||||||
|
Ok(()) => {
|
||||||
|
println!(
|
||||||
|
"cargo:warning=build_config.json was missing and has been created from build_config.default.json"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
panic!(
|
||||||
|
"failed to create {} from embedded defaults: {}",
|
||||||
|
config_path.display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_embedded_schema() {
|
||||||
|
let schema_json = parse_json_str(CONFIG_SCHEMA_STR, "embedded build_config.schema.json");
|
||||||
|
|
||||||
|
match jsonschema::meta::validate(&schema_json) {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(err) => {
|
||||||
|
panic!("embedded build_config.schema.json is not a valid JSON Schema: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_default_config() {
|
||||||
|
validate_config_str_against_schema(DEFAULT_CONFIG_STR, "embedded build_config.default.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_user_config(config_path: &Path) {
|
||||||
|
let raw = match fs::read_to_string(config_path) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(err) => {
|
||||||
|
panic!("failed to read {}: {}", config_path.display(), err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
validate_config_str_against_schema(&raw, &format!("{}", config_path.display()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_config_str_against_schema(config_raw: &str, config_name: &str) {
|
||||||
|
let schema_json = parse_json_str(CONFIG_SCHEMA_STR, "embedded build_config.schema.json");
|
||||||
|
let config_json = parse_json_str(config_raw, config_name);
|
||||||
|
|
||||||
|
let validator = match jsonschema::validator_for(&schema_json) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(err) => {
|
||||||
|
panic!(
|
||||||
|
"failed to build JSON Schema validator for {}: {}",
|
||||||
|
config_name, err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if validator.is_valid(&config_json) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
for err in validator.iter_errors(&config_json) {
|
||||||
|
errors.push(format!(
|
||||||
|
"- {} at instance path {}",
|
||||||
|
err,
|
||||||
|
err.instance_path()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let joined = errors.join("\n");
|
||||||
|
panic!("{} failed schema validation:\n{}", config_name, joined);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_build_config(config_path: &Path) -> BuildConfig {
|
||||||
|
let raw = match fs::read_to_string(config_path) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(err) => {
|
||||||
|
panic!("failed to read {}: {}", config_path.display(), err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match serde_json::from_str::<BuildConfig>(&raw) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(err) => {
|
||||||
|
panic!(
|
||||||
|
"failed to deserialize {} into BuildConfig: {}",
|
||||||
|
config_path.display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_profile_name(build_config: &BuildConfig) -> String {
|
||||||
|
let env_profile = std::env::var("BUILD_CONFIG_PROFILE");
|
||||||
|
match env_profile {
|
||||||
|
Ok(v) => {
|
||||||
|
if v.trim().is_empty() {
|
||||||
|
build_config.active_profile.clone()
|
||||||
|
} else {
|
||||||
|
v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => build_config.active_profile.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_profile<'a>(build_config: &'a BuildConfig, profile_name: &str) -> &'a BuildProfile {
|
||||||
|
match build_config.profiles.get(profile_name) {
|
||||||
|
Some(v) => v,
|
||||||
|
None => {
|
||||||
|
let mut available = Vec::new();
|
||||||
|
for key in build_config.profiles.keys() {
|
||||||
|
available.push(key.clone());
|
||||||
|
}
|
||||||
|
panic!(
|
||||||
|
"active profile '{}' not found in build_config.json. available profiles: {}",
|
||||||
|
profile_name,
|
||||||
|
available.join(", ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_profile_paths(profile: &BuildProfile, profile_name: &str) {
|
||||||
|
ensure_existing_file(&profile.qt.qmake, profile_name, "qt.qmake");
|
||||||
|
ensure_existing_dir(&profile.qt.lib_dir, profile_name, "qt.lib_dir");
|
||||||
|
ensure_existing_dir(&profile.qt.qml_dir, profile_name, "qt.qml_dir");
|
||||||
|
ensure_existing_dir(&profile.qt.plugins_dir, profile_name, "qt.plugins_dir");
|
||||||
|
|
||||||
|
let mut index = 0usize;
|
||||||
|
while index < profile.link.rpaths.len() {
|
||||||
|
ensure_existing_dir(
|
||||||
|
&profile.link.rpaths[index],
|
||||||
|
profile_name,
|
||||||
|
&format!("link.rpaths[{index}]"),
|
||||||
|
);
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_existing_file(path_str: &str, profile_name: &str, field_name: &str) {
|
||||||
|
let path = Path::new(path_str);
|
||||||
|
if !path.exists() {
|
||||||
|
panic!(
|
||||||
|
"profile '{}' has invalid {}: path does not exist: {}",
|
||||||
|
profile_name,
|
||||||
|
field_name,
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !path.is_file() {
|
||||||
|
panic!(
|
||||||
|
"profile '{}' has invalid {}: path is not a file: {}",
|
||||||
|
profile_name,
|
||||||
|
field_name,
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_existing_dir(path_str: &str, profile_name: &str, field_name: &str) {
|
||||||
|
let path = Path::new(path_str);
|
||||||
|
if !path.exists() {
|
||||||
|
panic!(
|
||||||
|
"profile '{}' has invalid {}: path does not exist: {}",
|
||||||
|
profile_name,
|
||||||
|
field_name,
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !path.is_dir() {
|
||||||
|
panic!(
|
||||||
|
"profile '{}' has invalid {}: path is not a directory: {}",
|
||||||
|
profile_name,
|
||||||
|
field_name,
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_build_environment(profile: &BuildProfile) {
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("QMAKE", &profile.qt.qmake);
|
||||||
|
std::env::set_var("QT_VERSION_MAJOR", profile.qt.version_major.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("cargo:rerun-if-env-changed=BUILD_CONFIG_PROFILE");
|
||||||
|
println!("cargo:rerun-if-env-changed=QMAKE");
|
||||||
|
println!("cargo:rerun-if-env-changed=QT_VERSION_MAJOR");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_linker_settings(profile: &BuildProfile) {
|
||||||
|
let mut index = 0usize;
|
||||||
|
while index < profile.link.rpaths.len() {
|
||||||
|
println!(
|
||||||
|
"cargo:rustc-link-arg=-Wl,-rpath,{}",
|
||||||
|
profile.link.rpaths[index]
|
||||||
|
);
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_runtime_hints(profile: &BuildProfile, profile_name: &str) {
|
||||||
|
println!(
|
||||||
|
"cargo:warning=using build profile '{}' with qmake '{}'",
|
||||||
|
profile_name, profile.qt.qmake
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"cargo:warning=qt lib_dir='{}', qml_dir='{}', plugins_dir='{}'",
|
||||||
|
profile.qt.lib_dir, profile.qt.qml_dir, profile.qt.plugins_dir
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_json_str(raw: &str, label: &str) -> Value {
|
||||||
|
match serde_json::from_str::<Value>(raw) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(err) => {
|
||||||
|
panic!("failed to parse {} as JSON: {}", label, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=build.rs");
|
||||||
|
println!("cargo:rerun-if-changed=build_config.default.json");
|
||||||
|
println!("cargo:rerun-if-changed=build_config.schema.json");
|
||||||
|
println!("cargo:rerun-if-changed=qml/main.qml");
|
||||||
|
|
||||||
|
let config_path = PathBuf::from("build_config.json");
|
||||||
|
|
||||||
|
ensure_user_config_exists(&config_path);
|
||||||
|
validate_embedded_schema();
|
||||||
|
validate_default_config();
|
||||||
|
validate_user_config(&config_path);
|
||||||
|
|
||||||
|
let build_config = load_build_config(&config_path);
|
||||||
|
let profile_name = resolve_profile_name(&build_config);
|
||||||
|
let profile = resolve_profile(&build_config, &profile_name);
|
||||||
|
|
||||||
|
validate_profile_paths(profile, &profile_name);
|
||||||
|
apply_build_environment(profile);
|
||||||
|
apply_linker_settings(profile);
|
||||||
|
|
||||||
|
CxxQtBuilder::new_qml_module(QmlModule::new("com.sasedev.cam02").qml_files(["qml/main.qml"]))
|
||||||
|
.qt_module("Qml")
|
||||||
|
.qt_module("Network")
|
||||||
|
.qt_module("Multimedia")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
emit_runtime_hints(profile, &profile_name);
|
||||||
|
}
|
||||||
33
poc004/build_config.default.json
Normal file
33
poc004/build_config.default.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"active_profile": "sinus",
|
||||||
|
"profiles": {
|
||||||
|
"sinus": {
|
||||||
|
"qt": {
|
||||||
|
"qmake": "/home/sinus/DEV/Qt/6.11.0/gcc_64/bin/qmake",
|
||||||
|
"lib_dir": "/home/sinus/DEV/Qt/6.11.0/gcc_64/lib",
|
||||||
|
"qml_dir": "/home/sinus/DEV/Qt/6.11.0/gcc_64/qml",
|
||||||
|
"plugins_dir": "/home/sinus/DEV/Qt/6.11.0/gcc_64/plugins",
|
||||||
|
"version_major": 6
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"rpaths": [
|
||||||
|
"/home/sinus/DEV/Qt/6.11.0/gcc_64/lib"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"system": {
|
||||||
|
"qt": {
|
||||||
|
"qmake": "/usr/bin/qmake6",
|
||||||
|
"lib_dir": "/usr/lib/x86_64-linux-gnu",
|
||||||
|
"qml_dir": "/usr/lib/x86_64-linux-gnu/qt6/qml",
|
||||||
|
"plugins_dir": "/usr/lib/x86_64-linux-gnu/qt6/plugins",
|
||||||
|
"version_major": 6
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
"rpaths": [
|
||||||
|
"/usr/lib/x86_64-linux-gnu"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
188
poc004/qml/main.qml
Normal file
188
poc004/qml/main.qml
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
// file: poc-qt/poc004/qml/main.qml
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtMultimedia
|
||||||
|
|
||||||
|
ApplicationWindow {
|
||||||
|
visible: true
|
||||||
|
width: 1100
|
||||||
|
height: 760
|
||||||
|
title: "POC004 - Webcam + Micro + Capture + Record"
|
||||||
|
|
||||||
|
MediaDevices {
|
||||||
|
id: mediaDevices
|
||||||
|
}
|
||||||
|
|
||||||
|
Camera {
|
||||||
|
id: camera
|
||||||
|
cameraDevice: mediaDevices.defaultVideoInput
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioInput {
|
||||||
|
id: microphone
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageCapture {
|
||||||
|
id: imageCapture
|
||||||
|
onImageSaved: function(requestId, filePath) {
|
||||||
|
statusLabel.text = "Image saved: " + filePath
|
||||||
|
}
|
||||||
|
onErrorOccurred: function(requestId, error, errorString) {
|
||||||
|
statusLabel.text = "Image error: " + errorString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaRecorder {
|
||||||
|
id: recorder
|
||||||
|
quality: MediaRecorder.VeryHighQuality
|
||||||
|
onRecorderStateChanged: {
|
||||||
|
if (recorder.recorderState === MediaRecorder.RecordingState) {
|
||||||
|
statusLabel.text = "Recording video..."
|
||||||
|
} else if (recorder.recorderState === MediaRecorder.StoppedState) {
|
||||||
|
statusLabel.text = "Recorder stopped"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onErrorOccurred: function(error, errorString) {
|
||||||
|
statusLabel.text = "Recorder error: " + errorString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CaptureSession {
|
||||||
|
id: captureSession
|
||||||
|
camera: camera
|
||||||
|
audioInput: microphone
|
||||||
|
imageCapture: imageCapture
|
||||||
|
recorder: recorder
|
||||||
|
videoOutput: videoOutput
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 12
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
color: "black"
|
||||||
|
border.width: 1
|
||||||
|
border.color: "#555"
|
||||||
|
|
||||||
|
VideoOutput {
|
||||||
|
id: videoOutput
|
||||||
|
anchors.fill: parent
|
||||||
|
fillMode: VideoOutput.PreserveAspectFit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: camera.active ? "Stop camera" : "Start camera"
|
||||||
|
onClicked: {
|
||||||
|
camera.active = !camera.active
|
||||||
|
if (camera.active) {
|
||||||
|
statusLabel.text = "Camera started"
|
||||||
|
} else {
|
||||||
|
statusLabel.text = "Camera stopped"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "Take snapshot"
|
||||||
|
onClicked: {
|
||||||
|
imageCapture.captureToFile("/home/sinus/Projects/poc-qt/target/captures_imgs/image_" + Date.now() + ".jpg")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: recorder.recorderState === MediaRecorder.RecordingState ? "Stop recording" : "Start recording"
|
||||||
|
onClicked: {
|
||||||
|
if (recorder.recorderState === MediaRecorder.RecordingState) {
|
||||||
|
recorder.stop()
|
||||||
|
} else {
|
||||||
|
recorder.outputLocation = "file:///home/sinus/Projects/poc-qt/target/captures_vids/video_" + Date.now() + ".mp4"
|
||||||
|
recorder.record()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "Restart camera"
|
||||||
|
onClicked: {
|
||||||
|
camera.stop()
|
||||||
|
camera.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: mediaDevices.videoInputs.length > 0
|
||||||
|
? ("Camera: " + camera.cameraDevice.description)
|
||||||
|
: "No camera detected"
|
||||||
|
Layout.fillWidth: true
|
||||||
|
wrapMode: Text.WrapAnywhere
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: "Mic active"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: statusLabel
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: "Ready"
|
||||||
|
wrapMode: Text.WrapAnywhere
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 120
|
||||||
|
clip: true
|
||||||
|
model: mediaDevices.videoInputs
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
width: ListView.view.width
|
||||||
|
height: 32
|
||||||
|
color: camera.cameraDevice.id === modelData.id ? "#334" : "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 8
|
||||||
|
text: modelData.description
|
||||||
|
color: "white"
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onClicked: {
|
||||||
|
camera.stop()
|
||||||
|
camera.cameraDevice = modelData
|
||||||
|
camera.start()
|
||||||
|
statusLabel.text = "Switched camera to: " + modelData.description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (mediaDevices.videoInputs.length > 0) {
|
||||||
|
camera.start()
|
||||||
|
statusLabel.text = "Camera started"
|
||||||
|
} else {
|
||||||
|
statusLabel.text = "No camera detected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
poc004/src/main.rs
Normal file
54
poc004/src/main.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
// file: poc-qt/poc004/src/main.rs
|
||||||
|
|
||||||
|
use cxx_qt_lib::{QGuiApplication, QQmlApplicationEngine, QString, QUrl};
|
||||||
|
use std::env;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut app = QGuiApplication::new();
|
||||||
|
let mut engine = QQmlApplicationEngine::new();
|
||||||
|
|
||||||
|
let current_dir = match env::current_dir() {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("failed to get current_dir: {err}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let main_qml_path: PathBuf = current_dir.join("qml/main.qml");
|
||||||
|
let main_qml_path = match main_qml_path.canonicalize() {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!(
|
||||||
|
"failed to resolve qml/main.qml from {}: {err}",
|
||||||
|
main_qml_path.display()
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let main_qml_str = match main_qml_path.to_str() {
|
||||||
|
Some(v) => v.to_string(),
|
||||||
|
None => {
|
||||||
|
eprintln!("qml path is not valid UTF-8: {}", main_qml_path.display());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("loading qml from: {}", main_qml_str);
|
||||||
|
|
||||||
|
if let Some(engine_ref) = engine.as_mut() {
|
||||||
|
let qml_url = QUrl::from_local_file(&QString::from(main_qml_str));
|
||||||
|
engine_ref.load(&qml_url);
|
||||||
|
} else {
|
||||||
|
eprintln!("failed to create QQmlApplicationEngine");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(app_ref) = app.as_mut() {
|
||||||
|
app_ref.exec();
|
||||||
|
} else {
|
||||||
|
eprintln!("failed to create QGuiApplication");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user