webcam from gstreamer
v4l2src -> videoconvert -> glupload -> glcolorconvert -> qml6glsink
This commit is contained in:
@@ -7,11 +7,12 @@ members = [
|
||||
"poc003",
|
||||
"poc004",
|
||||
"poc005",
|
||||
"poc006",
|
||||
]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/poc-qt"
|
||||
@@ -27,6 +28,7 @@ gstreamer = { version = "^0.25", features = ["serde"] }
|
||||
gstreamer-app = { version = "^0.25", features = [] }
|
||||
gstreamer-video = { version = "^0.25", features = ["serde"] }
|
||||
jsonschema = { version = "^0.40", features = [] }
|
||||
pkg-config = { version = "^0.3", features = [] }
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = { version = "^1.0", features = [] }
|
||||
tokio = { version = "^1.50", features = ["full"] }
|
||||
|
||||
25
poc006/Cargo.toml
Normal file
25
poc006/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
# file: poc-qt/poc006/Cargo.toml
|
||||
|
||||
[package]
|
||||
name = "poc-qt006"
|
||||
version = "0.6.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 = [] }
|
||||
pkg-config = { 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"] }
|
||||
glib = { workspace = true, features = [] }
|
||||
gstreamer = { workspace = true, features = ["serde"] }
|
||||
gstreamer-app = { workspace = true, features = [] }
|
||||
gstreamer-video = { workspace = true, features = ["serde"] }
|
||||
|
||||
319
poc006/build.rs
Normal file
319
poc006/build.rs
Normal file
@@ -0,0 +1,319 @@
|
||||
// file: poc-qt/poc006/build.rs
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use cxx_qt_build::CxxQtBuilder;
|
||||
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=src/qt_gst.rs");
|
||||
println!("cargo:rerun-if-changed=src/qt_gst_helpers.h");
|
||||
println!("cargo:rerun-if-changed=src/qt_gst_helpers.cpp");
|
||||
println!("cargo:rerun-if-changed=qml/main.qml");
|
||||
|
||||
let gst = match pkg_config::Config::new().probe("gstreamer-1.0") {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
panic!("failed to probe gstreamer-1.0 via pkg-config: {err}");
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
let mut builder = CxxQtBuilder::new()
|
||||
.file("src/qt_gst.rs")
|
||||
.cpp_file("src/qt_gst_helpers.cpp")
|
||||
.qt_module("Core")
|
||||
.qt_module("Gui")
|
||||
.qt_module("Qml")
|
||||
.qt_module("Quick");
|
||||
|
||||
unsafe {
|
||||
builder = builder.cc_builder(|cc| {
|
||||
cc.include("src");
|
||||
for include_path in &gst.include_paths {
|
||||
cc.include(include_path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
builder.build();
|
||||
|
||||
emit_runtime_hints(profile, &profile_name);
|
||||
}
|
||||
19
poc006/build_config.default.json
Normal file
19
poc006/build_config.default.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"active_profile": "system",
|
||||
"profiles": {
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
35
poc006/qml/main.qml
Normal file
35
poc006/qml/main.qml
Normal file
@@ -0,0 +1,35 @@
|
||||
// file: poc-qt/poc006/qml/main.qml
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import org.freedesktop.gstreamer.Qt6GLVideoItem 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
visible: true
|
||||
width: 960
|
||||
height: 700
|
||||
title: "POC006 - GStreamer qml6glsink"
|
||||
|
||||
color: "black"
|
||||
|
||||
GstGLQt6VideoItem {
|
||||
id: video
|
||||
objectName: "videoItem"
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: 48
|
||||
color: "#66000000"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
color: "white"
|
||||
text: "POC006 - webcam preview via GStreamer qml6glsink"
|
||||
}
|
||||
}
|
||||
}
|
||||
149
poc006/src/main.rs
Normal file
149
poc006/src/main.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
// file: poc006/src/main.rs
|
||||
|
||||
mod qt_gst;
|
||||
|
||||
use cxx_qt_lib::{QGuiApplication, QQmlApplicationEngine, QString, QUrl};
|
||||
use glib::translate::ToGlibPtr;
|
||||
use gstreamer as gst;
|
||||
use gst::prelude::*;
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = gst::init() {
|
||||
eprintln!("gst init failed: {err}");
|
||||
return;
|
||||
}
|
||||
|
||||
let pipeline = gst::Pipeline::new();
|
||||
|
||||
let src = match gst::ElementFactory::make("v4l2src").build() {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
eprintln!("failed to create v4l2src: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let videoconvert = match gst::ElementFactory::make("videoconvert").build() {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
eprintln!("failed to create videoconvert: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let glupload = match gst::ElementFactory::make("glupload").build() {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
eprintln!("failed to create glupload: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let glcolorconvert = match gst::ElementFactory::make("glcolorconvert").build() {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
eprintln!("failed to create glcolorconvert: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Important : créer qml6glsink avant le chargement du QML
|
||||
let sink = match gst::ElementFactory::make("qml6glsink").build() {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
eprintln!("failed to create qml6glsink: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = pipeline.add_many([&src, &videoconvert, &glupload, &glcolorconvert, &sink]) {
|
||||
eprintln!("failed to add elements to pipeline: {err}");
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(err) =
|
||||
gst::Element::link_many([&src, &videoconvert, &glupload, &glcolorconvert, &sink])
|
||||
{
|
||||
eprintln!("failed to link pipeline: {err}");
|
||||
let _ = pipeline.set_state(gst::State::Null);
|
||||
return;
|
||||
}
|
||||
|
||||
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}");
|
||||
let _ = pipeline.set_state(gst::State::Null);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let qml_path: PathBuf = current_dir.join("qml/main.qml");
|
||||
let qml_path = match qml_path.canonicalize() {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
eprintln!("failed to resolve qml/main.qml: {err}");
|
||||
let _ = pipeline.set_state(gst::State::Null);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let qml_path_str = match qml_path.to_str() {
|
||||
Some(v) => v.to_string(),
|
||||
None => {
|
||||
eprintln!("qml path is not valid UTF-8: {}", qml_path.display());
|
||||
let _ = pipeline.set_state(gst::State::Null);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
println!("loading qml from: {}", qml_path_str);
|
||||
|
||||
if let Some(mut engine_ref) = engine.as_mut() {
|
||||
let qml_url = QUrl::from_local_file(&QString::from(qml_path_str));
|
||||
engine_ref.as_mut().load(&qml_url);
|
||||
|
||||
let video_item = qt_gst::ffi::find_video_item(engine_ref.as_mut());
|
||||
if video_item.is_null() {
|
||||
eprintln!("failed to find QML videoItem");
|
||||
let _ = pipeline.set_state(gst::State::Null);
|
||||
return;
|
||||
}
|
||||
|
||||
let sink_ptr =
|
||||
<gst::Element as ToGlibPtr<'_, *mut gst::ffi::GstElement>>::to_glib_none(&sink).0
|
||||
as usize;
|
||||
|
||||
let ok = unsafe { qt_gst::ffi::bind_sink_widget(sink_ptr, video_item) };
|
||||
if !ok {
|
||||
eprintln!("failed to bind qml6glsink widget");
|
||||
let _ = pipeline.set_state(gst::State::Null);
|
||||
return;
|
||||
}
|
||||
|
||||
let pipeline_ptr =
|
||||
<gst::Pipeline as ToGlibPtr<'_, *mut gst::ffi::GstPipeline>>::to_glib_none(&pipeline).0
|
||||
as usize;
|
||||
|
||||
if !qt_gst::ffi::schedule_pipeline_play(engine_ref.as_mut(), pipeline_ptr) {
|
||||
eprintln!("failed to schedule pipeline play");
|
||||
let _ = pipeline.set_state(gst::State::Null);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
eprintln!("failed to create QQmlApplicationEngine");
|
||||
let _ = pipeline.set_state(gst::State::Null);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(app_ref) = app.as_mut() {
|
||||
app_ref.exec();
|
||||
}
|
||||
|
||||
let _ = pipeline.set_state(gst::State::Null);
|
||||
}
|
||||
18
poc006/src/qt_gst.rs
Normal file
18
poc006/src/qt_gst.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
// file: poc-qt/poc006/src/qt_gst.rs
|
||||
|
||||
#[cxx::bridge]
|
||||
pub mod ffi {
|
||||
unsafe extern "C++" {
|
||||
include!("qt_gst_helpers.h");
|
||||
|
||||
type QQmlApplicationEngine = cxx_qt_lib::QQmlApplicationEngine;
|
||||
type QQuickItem;
|
||||
|
||||
fn find_video_item(engine: Pin<&mut QQmlApplicationEngine>) -> *mut QQuickItem;
|
||||
unsafe fn bind_sink_widget(sink_ptr: usize, item: *mut QQuickItem) -> bool;
|
||||
fn schedule_pipeline_play(
|
||||
engine: Pin<&mut QQmlApplicationEngine>,
|
||||
pipeline_ptr: usize,
|
||||
) -> bool;
|
||||
}
|
||||
}
|
||||
95
poc006/src/qt_gst_helpers.cpp
Normal file
95
poc006/src/qt_gst_helpers.cpp
Normal file
@@ -0,0 +1,95 @@
|
||||
// file: poc-qt/poc006/src/qt_gst_helpers.cpp
|
||||
|
||||
#include "qt_gst_helpers.h"
|
||||
|
||||
#include <QQuickItem>
|
||||
#include <QQuickWindow>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QObject>
|
||||
#include <QRunnable>
|
||||
|
||||
#include <gst/gst.h>
|
||||
|
||||
class SetPlaying : public QRunnable
|
||||
{
|
||||
public:
|
||||
explicit SetPlaying(GstElement* pipeline)
|
||||
: pipeline_(pipeline ? GST_ELEMENT(gst_object_ref(pipeline)) : nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
~SetPlaying() override
|
||||
{
|
||||
if (pipeline_) {
|
||||
gst_object_unref(pipeline_);
|
||||
pipeline_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void run() override
|
||||
{
|
||||
if (pipeline_) {
|
||||
gst_element_set_state(pipeline_, GST_STATE_PLAYING);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
GstElement* pipeline_;
|
||||
};
|
||||
|
||||
QQuickItem* find_video_item(QQmlApplicationEngine& engine)
|
||||
{
|
||||
const auto roots = engine.rootObjects();
|
||||
if (roots.isEmpty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto* root_window = qobject_cast<QQuickWindow*>(roots.first());
|
||||
if (!root_window) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return root_window->findChild<QQuickItem*>("videoItem");
|
||||
}
|
||||
|
||||
bool bind_sink_widget(std::uintptr_t sink_ptr, QQuickItem* item)
|
||||
{
|
||||
if (sink_ptr == 0 || item == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* sink = reinterpret_cast<GstElement*>(sink_ptr);
|
||||
|
||||
g_object_set(sink, "widget", item, nullptr);
|
||||
|
||||
// Recommandé pour propager correctement le contexte GL Qt/GStreamer
|
||||
gst_element_set_state(sink, GST_STATE_READY);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool schedule_pipeline_play(QQmlApplicationEngine& engine, std::uintptr_t pipeline_ptr)
|
||||
{
|
||||
if (pipeline_ptr == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto roots = engine.rootObjects();
|
||||
if (roots.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* root_window = qobject_cast<QQuickWindow*>(roots.first());
|
||||
if (!root_window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* pipeline = reinterpret_cast<GstElement*>(pipeline_ptr);
|
||||
|
||||
root_window->scheduleRenderJob(
|
||||
new SetPlaying(pipeline),
|
||||
QQuickWindow::BeforeSynchronizingStage
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
12
poc006/src/qt_gst_helpers.h
Normal file
12
poc006/src/qt_gst_helpers.h
Normal file
@@ -0,0 +1,12 @@
|
||||
// file: poc-qt/poc006/src/qt_gst_helpers.h
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
class QQuickItem;
|
||||
class QQmlApplicationEngine;
|
||||
|
||||
QQuickItem* find_video_item(QQmlApplicationEngine& engine);
|
||||
bool bind_sink_widget(std::uintptr_t sink_ptr, QQuickItem* item);
|
||||
bool schedule_pipeline_play(QQmlApplicationEngine& engine, std::uintptr_t pipeline_ptr);
|
||||
Reference in New Issue
Block a user