Compare commits

...

79 commits
list ... main

Author SHA1 Message Date
Stéphane Cerveau 1f200f4f30 app: update to gtk 0.8.0 and gst 0.22.2
In the change, the glib::channel has been dropped to use
async_io which achieves the same to receive message
for the logger and display it in the treeview.
Another message is received when a new gtkpaintablesink
has been instanciated.
2024-03-14 16:34:55 +01:00
Stéphane Cerveau 44d64ccdc4 cargo: update gtk and gst crate
gtk4 0.7.3
gst 0.21.3
gst-plugins-gtk 0.11.3
2024-01-25 20:04:38 +01:00
Stéphane Cerveau 3985458832 properties: can now support ParamSpecFloat 2024-01-23 10:29:57 +01:00
Stéphane Cerveau 7a70feedba pages: update the pages with release 0.3.5 2024-01-05 16:34:13 +01:00
Stéphane Cerveau 88afa4a99e release: 0.3.5 2024-01-05 15:36:00 +01:00
Stéphane Cerveau d32c75b639 ci: change to macos 13 runner 2024-01-05 14:37:13 +01:00
Stéphane Cerveau 7789588aef ci: fix yaml issue detected by pre-commit 2024-01-05 11:25:03 +01:00
Stéphane Cerveau b1ad0e958b graphmanager: fix typos detected by pre-commit 2024-01-05 11:25:03 +01:00
Stéphane Cerveau 9c8a578e05 gps: fix typos and format detected by pre-commit 2024-01-05 11:25:03 +01:00
Stéphane Cerveau 3e15b7cecb graphmanager: remove link export 2024-01-05 11:25:03 +01:00
Stéphane Cerveau 11cf962bfd ci: add pre-commit job 2024-01-05 11:25:03 +01:00
Stéphane Cerveau a25f0499c8 macos: disabled ges build
Due to an error with python, disable GES build
for now.
2024-01-05 09:32:40 +01:00
Stéphane Cerveau 51769d6061 logger: support variable length for logger split 2024-01-04 15:20:14 +00:00
Stéphane Cerveau af317eee96 app: rename gst_pipeline_studio to gst-pipeline-studio 2024-01-04 14:32:17 +01:00
Stéphane Cerveau 3aded523c2 element: add property special case for float 2024-01-03 13:52:04 +01:00
Stéphane Cerveau d3005335b8 logger: add function name in the logs 2024-01-03 13:25:49 +01:00
Stéphane Cerveau 18458e3465 properties: fix expect for ParamSpecFlags 2024-01-03 13:25:49 +01:00
Stéphane Cerveau a6f03db8f6 element: element property special convert for enum and flags
In element_property, add special case enum and flags
property to use a better value such as nick or the
proper u32 flags
2024-01-03 13:25:49 +01:00
Stéphane Cerveau bed8d6a58e logger: fix clippy complain 2024-01-02 14:51:38 +01:00
Stéphane Cerveau c5f9cac444 app: support cmd line to open a custom pipeline
Add a way to open a pipeline from the cmd line
2024-01-02 14:51:03 +01:00
Stéphane Cerveau 30baa56881 wix: installer to a different UI profile
In order to remove minimal and full option
uses only a minimal profile.
2023-12-22 15:08:18 +01:00
Stéphane Cerveau 13165fa9c0 ci: use release build for windows
Create an image with release build of
gstreamer and gtk
2023-12-22 11:40:59 +01:00
Stéphane Cerveau 6ca3059914 ci: set the bundler version to 2.4.22 2023-12-19 11:20:56 +01:00
Stéphane Cerveau f4019fd2af wix: change wix banner and dialog bmp 2023-12-19 10:29:32 +01:00
Stéphane Cerveau 8c6cda2e92 player: feed the app with gst logs
Get the debug callback to retrieve the gst logs
and display it in the debug tab.
2023-11-30 11:54:15 +01:00
Stéphane Cerveau 9c03de5d00 player: keep a reference to bus_watch_guard
In order to receive the message from the bus
the API enforces to keep a reference to the bus_watch_guard
otherwise the watch gets lost.
2023-11-30 11:52:28 +01:00
Stéphane Cerveau 24121856ee app: use the channel to receive other logs
Add another logger to receive message, events or GST logs
2023-11-30 11:52:28 +01:00
Stéphane Cerveau e410289a13 ui: support multiple tab in debug section
Add GST logs, message, events in the debug section
2023-11-29 23:16:06 +01:00
Stéphane Cerveau 2601454143 Update README.md to include the website 2023-10-05 12:28:41 +00:00
Stéphane Cerveau f47b0624fd appdata: add a release description 2023-09-29 15:48:02 +02:00
Stéphane Cerveau b2c6a8bc2a pages: update the pages with release 0.3.4 2023-09-28 19:06:06 +02:00
Stéphane Cerveau d002e2811f release: 0.3.4 2023-09-28 16:45:01 +02:00
Stéphane Cerveau 0148a43946 windows: install the share folder from gtk/gst 2023-09-28 16:45:01 +02:00
Stéphane Cerveau e12fecf971 settings: create the default app folder
To avoid a crash if the settings folder is
not present and the log can not be created properly
2023-09-28 16:45:01 +02:00
Stéphane Cerveau 886c099dba data: update homepage in appdata
This appdata will be the mirror of what we
can see on
https://flathub.org/apps/org.freedesktop.dabrain34.GstPipelineStudio
2023-09-28 16:44:42 +02:00
Stéphane Cerveau f81bedb71a data: update the screenshot for 0.3.3 2023-09-24 12:57:09 +02:00
Stéphane Cerveau 2787211f0d release: update release version fetcher 2023-09-23 09:05:30 +02:00
Stéphane Cerveau 98d6451e74 pages: update the pages with release 0.3.3 2023-09-22 15:32:09 +02:00
Stéphane Cerveau 970103ddee version: 0.3.3 2023-09-22 15:03:45 +02:00
Stéphane Cerveau aa551db066 ci: update macos job to start build earlier 2023-09-22 14:33:26 +02:00
Stéphane Cerveau 4569fa79e8 meson: set minimum version of meson==0.63.0 2023-09-22 14:33:26 +02:00
Stéphane Cerveau be2d7ab6d0 macos: copy gtk4 dependencies 2023-09-22 14:32:29 +02:00
Stéphane Cerveau cf438b523c ci: pages test removed 2023-09-20 22:24:25 +02:00
Stéphane Cerveau 66cd1b9c15 app: use default size to save/load app size
Use maximize on all platform except
macos

Fixes #19
2023-09-20 22:20:10 +02:00
Stéphane Cerveau 1789bb0b25 app: remove the maximize call
The maximize call blocks the resize on MacOS.

Fixes #19
2023-09-20 13:34:16 +00:00
Stéphane Cerveau c1ae2c37b6 ci: add gitlab pages jobs 2023-09-19 13:00:20 +02:00
Stéphane Cerveau a32b16c467 release: update the description 2023-09-18 16:08:19 +02:00
Stéphane Cerveau 06b0ec8ead graphbook: remove double call to set_tooltip_markup 2023-09-18 15:57:54 +02:00
Stéphane Cerveau d0bf6091a4 update to version 0.3.2 2023-09-18 13:55:38 +02:00
Stéphane Cerveau f4be2299b9 graphbook: introduce element_factory_exists
This method helps to tell if a factory exists
when loading a graph.
If the factory does not exists, use light mode
to display it and prevent some menu item
and change its description.
2023-09-18 12:34:58 +02:00
Stéphane Cerveau 111750a33b build-aux: use copy2 instead of copyfile 2023-09-18 12:33:25 +02:00
Stéphane Cerveau 40e999f7d1 installers: add VERSION file 2023-09-18 12:33:25 +02:00
Stéphane Cerveau dd4700eb11 ci: add linux release 2023-09-17 17:52:45 +02:00
Stéphane Cerveau f2cc9ea886 meson: change minimum requirement for gst
Use gst 1.22
2023-09-17 16:20:50 +02:00
Stéphane Cerveau 5c9a273fcc version: prepare v0.3.1 2023-09-17 16:20:50 +02:00
Stéphane Cerveau 38928e07b8 macos: get tag for version 2023-09-16 10:37:59 +02:00
Stéphane Cerveau 4a5964c340 app: manage graphtab as a tab of notebook
graph tab in the graphbook containing a player
and a graphview
2023-09-16 10:37:59 +02:00
Stéphane Cerveau bcc39acf5e app: add session structure
This structure represents a graphview and a player.
It will allow a future use of notebook
in the main window
2023-09-15 11:56:16 +02:00
Stéphane Cerveau 3fbaa3166d player: register external plugins only once
In case of multiple graph tab the player was registering
several time the gtk4paintablesink
2023-09-15 11:56:16 +02:00
Stéphane Cerveau 3689d4d57c graphview: change log level for link name 2023-09-15 11:56:16 +02:00
Stéphane Cerveau ea43d37589 changelog: update changelog and todo 2023-08-31 17:23:58 +02:00
Stéphane Cerveau eda453df53 graphview: use gtk::style_context_add_provider_for_display
gtk::StyleContext::add_provider_for_display is now deprecated
since gtk update to 0.7.2
2023-08-31 17:23:58 +02:00
Stéphane Cerveau 375f083cbf cargo: update rust, gtk and gstreamer version 2023-08-31 17:23:58 +02:00
Stéphane Cerveau a56c0956ab macos: remove useless comment in deploy 2023-08-26 21:42:58 +02:00
Stéphane Cerveau d2b1c8875e deploy_macos: copy gst plugin dep in lib 2023-08-26 21:00:29 +02:00
Stéphane Cerveau aa66eb37de macos: relocate the libraries with rpath 2023-08-26 19:50:33 +02:00
Stéphane Cerveau 0a48ff3d58 gstreamer: use stable 1.22 version
This version includes the patch for orc

https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/4866
2023-08-26 19:50:33 +02:00
Stéphane Cerveau 234b5fe043 ci: update fedora version to 38 2023-08-26 12:52:30 +02:00
Stéphane Cerveau 16db9ab30a app: remove special use case with caps filter 2023-08-26 12:52:30 +02:00
Stéphane Cerveau ad6c53a531 app: support of caps filter by link name 2023-08-26 12:52:30 +02:00
Stéphane Cerveau aaf47503fe graphmanager: add link name
Allow to set and display a link name
2023-08-26 12:52:30 +02:00
Stéphane Cerveau 5de51d5565 meson: add gtk4 as a subproject
Add gtk.wrap to subprojects

Remove gtk4 from brew_setup.sh
2023-06-14 22:35:22 +02:00
Stéphane Cerveau 21962866c5 ci: update fedora image with missing package
Add flex and bison
2023-06-14 22:35:21 +02:00
Stéphane Cerveau 14a872fb89 meson: add gstreamer 1.0 as a subproject 2023-06-14 22:35:20 +02:00
Stéphane Cerveau bb38972aea macos: deploying application
https://www.datatable.online/en/blog/004-how-to-deploy-gtk-app-on-mac.html#deploy-your-app-as-linux-app
2023-06-14 22:32:58 +02:00
Stéphane Cerveau e0274e8fd6 ci: fix flatpak release job 2023-04-14 11:27:37 +02:00
Stéphane Cerveau 1aa39f2c62 gps: update version to 0.3.0 2023-04-12 22:38:44 +02:00
Stéphane Cerveau 9c3ee809b1 ci: add windows release 2023-04-12 22:38:44 +02:00
Stéphane Cerveau 0748e9352b ci: use wix install from latest gstreamer ci image 2023-04-12 22:38:44 +02:00
66 changed files with 3674 additions and 1064 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
/target
*.log
src/config.rs
subprojects/*/

View file

@ -9,19 +9,20 @@ stages:
- lint
- test
- release
- deploy
variables:
FDO_UPSTREAM_REPO: "dabrain34/GstPipelineStudio"
GNOME_RUNTIME_IMAGE: "registry.gitlab.gnome.org/gnome/gnome-runtime-images/gnome:41"
WINDOWS_RUST_STABLE_IMAGE: "$CI_REGISTRY_IMAGE/windows:$GST_RS_IMG_TAG-main-$GST_RS_STABLE"
WINDOWS_RUST_STABLE_UPSTREAM_IMAGE: "$CI_REGISTRY/$FDO_UPSTREAM_REPO/windows:$GST_RS_IMG_TAG-main-$GST_RS_STABLE"
GNOME_RUNTIME_IMAGE: "quay.io/gnome_infrastructure/gnome-runtime-images:gnome-master"
WINDOWS_RUST_STABLE_IMAGE: "$CI_REGISTRY_IMAGE/windows:$GST_RS_WIN_IMG_TAG"
WINDOWS_RUST_STABLE_UPSTREAM_IMAGE: "$CI_REGISTRY/$FDO_UPSTREAM_REPO/windows:$GST_RS_WIN_IMG_TAG"
# Version and tag for our current container
.fedora:
variables:
FDO_DISTRIBUTION_VERSION: "36"
FDO_DISTRIBUTION_VERSION: "38"
# Update this to trigger a container rebuild
FDO_DISTRIBUTION_TAG: "2023-02-13.1"
FDO_DISTRIBUTION_TAG: $GST_RS_FDO_IMG_TAG
before_script:
- source ./ci/env.sh
- mkdir .cargo && echo -e "[net]\ngit-fetch-with-cli = true" > .cargo/config
@ -55,9 +56,12 @@ build-fedora-container:
xorg-x11-server-Xvfb
wget
git
flex
bison
FDO_DISTRIBUTION_EXEC: >-
ci/install-rust.sh stable &&
pip3 install meson
pip3 install meson &&
pip3 install pre-commit
.windows rust docker build:
stage: prepare
@ -68,12 +72,12 @@ build-fedora-container:
#
# We also don't need a CONTEXT_DIR var as its also
# hardcoded to be windows-docker/
DOCKERFILE: 'ci/windows-docker/Dockerfile'
GST_UPSTREAM_BRANCH: '1.22'
DOCKERFILE: "ci/windows-docker/Dockerfile"
GST_UPSTREAM_BRANCH: "1.22"
tags:
- 'windows'
- 'shell'
- '2022'
- "windows"
- "shell"
- "2022"
script:
# We need to pass an array and to resolve the env vars, so we can't use a variable:
- $DOCKER_BUILD_ARGS = @("--build-arg", "DEFAULT_GST_BRANCH=$GST_UPSTREAM_BRANCH", "--build-arg", "RUST_VERSION=$RUST_VERSION")
@ -85,49 +89,88 @@ build-fedora-container:
}
windows rust docker stable:
extends: '.windows rust docker build'
extends: ".windows rust docker build"
variables:
RUST_IMAGE: !reference [variables, "WINDOWS_RUST_STABLE_IMAGE"]
RUST_UPSTREAM_IMAGE: !reference [variables, "WINDOWS_RUST_STABLE_UPSTREAM_IMAGE"]
RUST_VERSION: !reference [variables, "GST_RS_STABLE"]
RUST_IMAGE: $WINDOWS_RUST_STABLE_IMAGE
RUST_UPSTREAM_IMAGE: $WINDOWS_RUST_STABLE_UPSTREAM_IMAGE"]
RUST_VERSION: $GST_RS_STABLE
.msvc2019 build:
stage: test
tags:
- 'docker'
- 'windows'
- '2022'
- "docker"
- "windows"
- "2022"
windows installer stable:
needs:
- job: 'windows rust docker stable'
- job: "windows rust docker stable"
artifacts: false
image: "$WINDOWS_RUST_STABLE_IMAGE"
extends: '.msvc2019 build'
extends: ".msvc2019 build"
script:
- rustc --version
- git fetch --tags
- "& ./ci/build_gps.ps1"
- "& ./installer/wix/prepare_wix.ps1"
- "& ./installer/wix/prepare_gstreamer.ps1"
- "& ./installer/wix/build_installer.ps1"
artifacts:
paths:
- installer/wix/*.msi
expire_in: 10 days
when: 'manual'
paths:
- installer/wix/*.msi
expire_in: 10 days
when: "manual"
rustfmt-clippy:
pre-commit:
stage: "lint"
extends:
- .fedora
- .fdo.distribution-image@fedora
variables:
PRE_COMMIT_HOME: ${CI_PROJECT_DIR}/.cache/pre-commit
cache:
paths:
- ${PRE_COMMIT_HOME}
script:
- meson setup build
- pre-commit run --all-files
clippy:
extends:
- .fedora
- .fdo.distribution-image@fedora
stage: lint
script:
- meson build
- cargo fmt --version
- cargo fmt -- --color=always --check
- cargo clippy --version
- cargo clippy --color=always --all-targets -- -D warnings
windows installer release:
extends: "windows installer stable"
stage: release
only:
- flatpak
- tags
artifacts:
paths:
- installer/wix/*.msi
when: "always"
linux release:
extends:
- .fedora
- .fdo.distribution-image@fedora
stage: release
only:
- flatpak
- tags
script:
- meson builddir -Dbuildtype=release
- ninja -C builddir/ dist
artifacts:
paths:
- builddir/meson-dist/*
when: "always"
test-stable:
extends:
- .fedora
@ -169,19 +212,11 @@ dist-package:
flatpak:
image: $GNOME_RUNTIME_IMAGE
stage: release
only:
- flatpak
- tags
# Using gstreamer runner avoids an issue with 'bwrap: No permissions to creating new namespace' during flatpak builder
tags:
- gstreamer
stage: test
variables:
BUNDLE: "gst-pipeline-studio-nightly.flatpak"
MANIFEST_PATH: "build-aux/org.freedesktop.dabrain34.GstPipelineStudio.Devel.json"
FLATPAK_MODULE: "gst-pipeline-studio"
APP_ID: "org.freedesktop.dabrain34.GstPipelineStudio.Devel"
RUNTIME_REPO: "https://nightly.gnome.org/gnome-nightly.flatpakrepo"
BUNDLE: "gst-pipeline-studio-nightly.flatpak"
script:
- flatpak-builder app ${MANIFEST_PATH}
- flatpak build-export repo app
@ -192,6 +227,70 @@ flatpak:
when: "always"
paths:
- "${BUNDLE}"
- ".flatpak-builder/build/${FLATPAK_MODULE}/_flatpak_build/meson-logs/meson-log.txt"
- ".flatpak-builder/build/${FLATPAK_MODULE}/_flatpak_build/meson-logs/testlog.txt"
expire_in: 14 days
cache:
key: "flatpak"
paths:
- .flatpak-builder/downloads/
- .flatpak-builder/git/
- target/
- target_test/
when: "manual"
macos installer stable:
stage: test
tags:
- gst-macos-13
before_script:
- pip3 install --upgrade pip
# Make sure meson is up to date
- pip3 install -U meson
# Need to install certificates for python
- pip3 install --upgrade certifi
# Another way to install certificates
- open /Applications/Python\ 3.8/Install\ Certificates.command
# Get ninja
- pip3 install -U ninja
script:
# rust toolchain
- curl https://sh.rustup.rs -sSf | sh -s -- -y
- source $HOME/.cargo/env
# brew install
- /bin/bash -c "./installer/macos/brew_setup.sh"
- /bin/bash -c "./installer/macos/deploy_macos.sh"
artifacts:
name: "MacOS installer"
paths:
- installer/**/GstPipelineStudio*.dmg
- installer/**/GstPipelineStudio*.tar.gz
expire_in: 14 days
when: "manual"
macos installer release:
extends: "macos installer stable"
stage: release
only:
- flatpak
- tags
artifacts:
name: "MacOS installer"
paths:
- installer/**/GstPipelineStudio*.dmg
- installer/**/GstPipelineStudio*.tar.gz
when: "always"
pages:
image: ruby:2.7
stage: deploy
script:
- gem install bundler -v 2.4.22
- bundle install
- bundle exec jekyll build -d public
artifacts:
paths:
# The folder that contains the files to be exposed at the Page URL
- public
rules:
# This ensures that only pushes to the default branch will trigger
# a pages deploy
- if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH

29
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,29 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.11
hooks:
# Run the linter.
- id: ruff
# Run the formatter.
- id: ruff-format
- repo: https://github.com/crate-ci/typos
rev: v1.17.0
hooks:
- id: typos
exclude: '^$|\.svg$'
- repo: https://github.com/doublify/pre-commit-rust
rev: v1.0
hooks:
- id: fmt

1076
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,15 @@
[package]
name = "gst_pipeline_studio"
version = "0.2.3"
name = "gst-pipeline-studio"
version = "0.3.5"
edition = "2018"
rust-version = "1.67"
rust-version = "1.70.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
gtk = { version = "0.6.1", package = "gtk4" }
gst = { package = "gstreamer", version = "0.20" }
gst-plugin-gtk4 = { version = "0.10", optional=true }
gtk = { version = "0.8.0", package = "gtk4" }
gst = { package = "gstreamer", version = "0.22.2" }
gst-plugin-gtk4 = { version = "0.12.1", optional=true }
anyhow = "1"
log = "0.4.11"
once_cell = "1.7.2"
@ -18,6 +18,11 @@ serde = "1.0"
serde_any = "0.5"
simplelog = "0.11.2"
futures-channel = "0.3"
lazy_static = "1.4"
chrono = "0.4"
structopt = "0.3"
async-channel = "2.0.0"
[dev-dependencies]
futures-executor = "0.3"

View file

@ -83,3 +83,45 @@
### Graphview
- [x] Update node description on property removal
## 0.3.0
### CI/Infra
- [x] Create a macos installer
- [x] Create a windows installer
### Graphview
- [x] set/get the file format version
### GStreamer
- [x] Display GStreamer version in the about dialog
## 0.3.1
### app
- [x] Add multiple graphviews with tabs.
- [x] handle the caps setter element
## 0.3.2
### app
- [x] check that element exists before creating it on file load.
## 0.3.3
### app
- [x] Fix MacOs GTK runtime dependencies
- [x] Fix the maximize call with MacOS
- [x] Fix the default size at GTK save/load state
## 0.3.4
### app
- [x] Fix first run when application folder has not been created, fixes #23
- [x] Fix windows installer to bring share folder and let filesrc work properly, fixes #24
## 0.3.5
### app
- [x] logs: receive multiple log sources such as GST logs and messages.
- [x] settings: add a log level selection
- [x] rename gst_pipeline_studio to gst-pipeline-studio
- [x] can open a pipeline from the command line

3
Gemfile Normal file
View file

@ -0,0 +1,3 @@
source "https://rubygems.org"
gem "jekyll"

View file

@ -1,9 +1,11 @@
# Important
# [GstPipelineStudio](https://dabrain34.pages.freedesktop.org/GstPipelineStudio): Draw your own GStreamer pipeline ...
## Important
Until version 1.0, this software should be considered as **unstable**.
The settings moreover the graph file format might change over the development phase.
# GstPipelineStudio: Draw your own GStreamer pipeline ...
## Description
@ -49,7 +51,7 @@ brew install gstreamer gst-plugins-base gst-plugins-bad
```sh
$ meson builddir -Dbuildtype=release
$ ninja -C builddir
$ ./builddir/target/release/gst_pipeline_studio
$ ./builddir/target/release/gst-pipeline-studio
```
## Flatpak

12
TODO.md
View file

@ -3,40 +3,32 @@
### Graphview
- [ ] create a crate for graphview/node/port
- [x] set/get the file format version
### GStreamer:
- [ ] Implement pipeline unit test
- [x] Display GStreamer version in the about dialog
### app
- [ ] Control the connection between element
- [ ] unable to connect element with incompatible caps.
- [ ] Add multiple graphviews with tabs.
- [ ] unable to connect element with incompatible caps.
- [ ] Implement graph dot render/load
- [ ] Implement a command line parser to graph
- [ ] handle the caps setter element
- [ ] Add probes on each pad to monitor the pipeline
- [ ] Render a media file
- [ ] Offer compatible element to a pad (autorender)
- [ ] Display tags/meta/message detected
- [ ] Change TreeView to ListView
- [ ] Implement zoom on the view (https://gitlab.gnome.org/World/obfuscate/-/blob/master/src/widgets/drawing_area.rs)
- [ ] Settings: add a log level selection
- [ ] reopen the last log on prematured exit (crash)
- [ ] Play/pause should be prevented until the pipeline is ready
- [ ] Filter the elements by class/rank etc.
- [ ] double click on node/pad open the properties
### CI/Infra
- [ ] Create a macos/windows job
## bugs
- [ ] check that element exists before creating it on file load.
- [ ] Combo box is not well selected if the value is not linear such as flags. See flags in playbin
- [ ] opening a graph file can lead a different behavior in the pipeline. See videomixer graph where the zorder
on pads is not correctly set to right one.

1
VERSION Normal file
View file

@ -0,0 +1 @@
0.3.5

View file

@ -7,26 +7,39 @@ import shutil
env = os.environ
MESON_BUILD_ROOT=sys.argv[1]
MESON_SOURCE_ROOT=sys.argv[2]
CARGO_TARGET_DIR = os.path.join (MESON_BUILD_ROOT, "target")
MESON_BUILD_ROOT = sys.argv[1]
MESON_SOURCE_ROOT = sys.argv[2]
CARGO_TARGET_DIR = os.path.join(MESON_BUILD_ROOT, "target")
env["CARGO_TARGET_DIR"] = CARGO_TARGET_DIR
CARGO_HOME = os.path.join (CARGO_TARGET_DIR, "cargo-home")
env["CARGO_HOME"] = CARGO_HOME
OUTPUT=sys.argv[3]
BUILDTYPE=sys.argv[4]
APP_BIN=sys.argv[5]
env["CARGO_HOME"] = os.path.join(CARGO_TARGET_DIR, "cargo-home")
OUTPUT = sys.argv[3]
BUILDTYPE = sys.argv[4]
APP_BIN = sys.argv[5]
env["PKG_CONFIG_PATH"] = (
os.path.join(MESON_BUILD_ROOT, "meson-uninstalled")
+ os.pathsep
+ env.get("PKG_CONFIG_PATH", "")
)
if BUILDTYPE == "release":
if BUILDTYPE == "release":
print("RELEASE MODE")
CMD = ['cargo', 'build', '--manifest-path', os.path.join(MESON_SOURCE_ROOT, 'Cargo.toml'), '--release']
CMD = [
"cargo",
"build",
"--manifest-path",
os.path.join(MESON_SOURCE_ROOT, "Cargo.toml"),
"--release",
]
subprocess.run(CMD, env=env)
shutil.copyfile(os.path.join(CARGO_TARGET_DIR, "release", APP_BIN), OUTPUT)
shutil.copy2(os.path.join(CARGO_TARGET_DIR, "release", APP_BIN), OUTPUT)
else:
print("DEBUG MODE")
CMD = ['cargo', 'build', '--manifest-path', os.path.join(MESON_SOURCE_ROOT, 'Cargo.toml')]
CMD = [
"cargo",
"build",
"--manifest-path",
os.path.join(MESON_SOURCE_ROOT, "Cargo.toml"),
]
subprocess.run(CMD, env=env)
shutil.copyfile(os.path.join(CARGO_TARGET_DIR, "debug", APP_BIN), OUTPUT)
shutil.copy2(os.path.join(CARGO_TARGET_DIR, "debug", APP_BIN), OUTPUT)

View file

@ -3,19 +3,17 @@
from os import environ, path
from subprocess import call
prefix = environ.get('MESON_INSTALL_PREFIX', '/usr/local')
datadir = path.join(prefix, 'share')
destdir = environ.get('DESTDIR', '')
prefix = environ.get("MESON_INSTALL_PREFIX", "/usr/local")
datadir = path.join(prefix, "share")
destdir = environ.get("DESTDIR", "")
# Package managers set this so we don't need to run
if not destdir:
print('Updating icon cache...')
call(['gtk-update-icon-cache', '-qtf', path.join(datadir, 'icons', 'hicolor')])
print('Updating desktop database...')
call(['update-desktop-database', '-q', path.join(datadir, 'applications')])
print('Compiling GSettings schemas...')
call(['glib-compile-schemas', path.join(datadir, 'glib-2.0', 'schemas')])
print("Updating icon cache...")
call(["gtk-update-icon-cache", "-qtf", path.join(datadir, "icons", "hicolor")])
print("Updating desktop database...")
call(["update-desktop-database", "-q", path.join(datadir, "applications")])
print("Compiling GSettings schemas...")
call(["glib-compile-schemas", path.join(datadir, "glib-2.0", "schemas")])

View file

@ -1,28 +1,56 @@
{
"app-id": "org.freedesktop.dabrain34.GstPipelineStudio.Devel",
"runtime": "org.gnome.Platform",
"runtime-version": "41",
"runtime-version": "master",
"sdk": "org.gnome.Sdk",
"sdk-extensions": ["org.freedesktop.Sdk.Extension.rust-stable"],
"command": "gst_pipeline_studio",
"sdk-extensions": [
"org.freedesktop.Sdk.Extension.rust-stable"
],
"command": "gst-pipeline-studio",
"finish-args": [
"--socket=fallback-x11",
"--socket=wayland",
"--device=dri",
"--share=ipc",
"--share=network",
"--filesystem=home",
"--env=G_MESSAGES_DEBUG=none",
"--env=RUST_BACKTRACE=1"
],
"build-options": {
"append-path": "/usr/lib/sdk/rust-stable/bin",
"build-args": ["--share=network"],
"test-args": ["--socket=x11", "--share=network"]
"build-args": [
"--share=network"
],
"test-args": [
"--socket=x11",
"--share=network"
]
},
"modules": [
{
"name": "gst_pipeline_studio",
"name": "gstreamer",
"buildsystem": "meson",
"builddir": true,
"config-opts": [
"-Ddoc=disabled"
],
"sources": [
{
"type": "git",
"tag": "1.22",
"url": "https://gitlab.freedesktop.org/gstreamer/gstreamer.git",
"disable-submodules": true
}
]
},
{
"name": "gst-pipeline-studio",
"buildsystem": "meson",
"run-tests": true,
"config-opts": ["-Dprofile=development"],
"config-opts": [
"-Dprofile=development"
],
"sources": [
{
"type": "dir",
@ -31,4 +59,4 @@
]
}
]
}
}

View file

@ -1,4 +1,4 @@
$env:MESON_ARGS = "--prefix=C:\gst-install\"
$env:MESON_ARGS = "--prefix=C:\gst-install\ -Dbuildtype=release"
cmd.exe /C "C:\BuildTools\Common7\Tools\VsDevCmd.bat -host_arch=amd64 -arch=amd64 && meson _build $env:MESON_ARGS && meson compile -C _build && ninja -C _build install"
if (!$?) {
Write-Host "Failed to build and install GstPipelineStudio"

View file

@ -1,4 +1,4 @@
variables:
GST_RS_IMG_TAG: '2023-04-12.0'
GST_RS_STABLE: '1.67.0'
GST_RS_MSRV: '1.63.0'
GST_RS_WIN_IMG_TAG: "2023-12-22.0"
GST_RS_FDO_IMG_TAG: "2024-01-05.0"
GST_RS_STABLE: "1.70.0"

View file

@ -1,6 +1,6 @@
source ./ci/env.sh
RUSTUP_VERSION=1.23.1
RUSTUP_VERSION=1.26.0
RUST_VERSION=$1
RUST_ARCH="x86_64-unknown-linux-gnu"

View file

@ -1,6 +1,6 @@
# escape=`
FROM "registry.freedesktop.org/gstreamer/gstreamer/amd64/windows:2022-09-23.0-main"
FROM "registry.freedesktop.org/gstreamer/gstreamer/amd64/windows:2023-08-24.0-main"
# Make sure any failure in PowerShell is fatal
ENV ErrorActionPreference='Stop'
@ -8,7 +8,6 @@ SHELL ["powershell","-NoLogo", "-NonInteractive", "-Command"]
ARG DEFAULT_GST_BRANCH="1.22"
ARG DEFAULT_GTK_BRANCH="4.8.2"
ARG DEFAULT_PANGO_BRANCH="1.50.14"
ARG RUST_VERSION="invalid"
RUN choco install -y pkgconfiglite nasm llvm
@ -17,8 +16,7 @@ RUN choco install -y pkgconfiglite nasm llvm
RUN setx PATH '%PATH%;C:\Program Files\NASM;C:\gst-install\bin;c:\Program Files\gettext-iconv\bin'
ENV PKG_CONFIG_PATH="C:\gst-install\lib\pkgconfig"
COPY install_pango.ps1 install_gst.ps1 install_gtk.ps1 C:\
RUN C:\install_pango.ps1
COPY install_gst.ps1 install_gtk.ps1 C:\
RUN C:\install_gst.ps1
RUN C:\install_gtk.ps1

View file

@ -26,7 +26,7 @@ if (!$?) {
Exit 1
}
$env:MESON_ARGS = "--prefix=C:\gst-install\ " +
$env:MESON_ARGS = "--prefix=C:\gst-install\ -Dbuildtype=release " +
"-Dglib:installed_tests=false " +
"-Dlibnice:tests=disabled " +
"-Dlibnice:examples=disabled " +

View file

@ -1,6 +1,6 @@
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;
$env:MESON_ARGS = "--prefix=C:\gst-install\"
$env:MESON_ARGS = "--prefix=C:\gst-install\ -Dbuildtype=release"
# Download gtk and all its subprojects
git clone -b $env:DEFAULT_GTK_BRANCH --depth 1 https://gitlab.gnome.org/gnome/gtk.git C:\gtk

573
data/css/style.css Normal file
View file

@ -0,0 +1,573 @@
/*
GstPipelineStudio Website
====================
shamelessly stolen CSS from Pipewire
*/
/* GNOME Color Palette */
:root {
--rounded-corner: 12px;
--blue1: rgb(153, 193, 241);
--blue2: rgb(98, 160, 234);
--blue3: rgb(53, 132, 228);
--blue4: rgb(28, 113, 216);
--blue5: rgb(26, 95, 180);
--green1: rgb(143, 240, 164);
--green2: rgb(87, 227, 137);
--green3: rgb(51, 209, 122);
--green4: rgb(46, 194, 126);
--green5: rgb(38, 162, 105);
--yellow1: rgb(249, 240, 107);
--yellow2: rgb(248, 228, 92);
--yellow3: rgb(246, 211, 45);
--yellow4: rgb(245, 194, 17);
--yellow5: rgb(229, 165, 10);
--orange1: rgb(255, 190, 111);
--orange2: rgb(255, 163, 72);
--orange3: rgb(255, 120, 0);
--orange4: rgb(230, 97, 0);
--orange5: rgb(198, 70, 0);
--red1: rgb(246, 97, 81);
--red2: rgb(237, 51, 59);
--red3: rgb(224, 27, 36);
--red4: rgb(192, 28, 40);
--red5: rgb(165, 29, 45);
--purple1: rgb(220, 138, 221);
--purple2: rgb(192, 97, 203);
--purple3: rgb(145, 65, 172);
--purple4: rgb(129, 61, 156);
--purple5: rgb(97, 53, 131);
--brown1: rgb(205, 171, 143);
--brown2: rgb(181, 131, 90);
--brown3: rgb(152, 106, 68);
--brown4: rgb(134, 94, 60);
--brown5: rgb(99, 69, 44);
--light1: rgb(255, 255, 255);
--light2: rgb(246, 245, 244);
--light3: rgb(222, 221, 218);
--light4: rgb(192, 191, 188);
--light5: rgb(154, 153, 150);
--dark1: rgb(119, 118, 123);
--dark2: rgb(94, 92, 100);
--dark3: rgb(61, 56, 70);
--dark4: rgb(36, 31, 49);
--dark5: rgb(0, 0, 0);
--primary-color: var(--blue5);
/* Set your project color */
--borders: var(--light3);
}
/* Typography */
@font-face {
font-family: 'Inter Var';
font-weight: 100 900;
font-display: swap;
font-style: oblique 0deg 10deg;
src: url("fonts/Inter.var.woff2?v=3.19") format("woff2");
}
* {
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
font-size: 16px;
font-family: "Inter Var", sans-serif;
font-weight: 400;
line-height: 1.6;
}
body {
color: #241f31;
background-color: #f6f5f4;
min-height: 100vh;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 2rem 0 -6px;
font-weight: 600;
line-height: 1.25;
font-variation-settings: "wght" 600;
/* needed for webkit */
}
h1 {
font-size: 30px;
font-weight: 100;
font-style: normal;
margin: 3rem 0 2rem;
}
@media screen and (min-width: 650px) {
h1 {
font-size: 38px;
}
}
h2 {
font-size: 1.2rem;
}
@media (prefers-color-scheme: dark) {
body {
filter: invert(100%) hue-rotate(180deg);
}
html {
background-color: var(--dark5);
}
img,
video,
iframe {
filter: invert(100%) hue-rotate(180deg);
}
}
a {
font-weight: 600;
text-decoration: none;
color: var(--primary-color);
cursor: pointer;
font-variation-settings: "wght" 600;
/* needed for webkit */
}
a:hover {
text-decoration: underline;
}
b {
font-weight: 600;
}
small {
color: #777;
}
hr {
margin: 3rem auto 4rem;
width: 40%;
opacity: 40%;
}
img {
display: block;
margin: 2rem auto;
max-width: 100%;
}
img.full {
width: 100%;
}
img.pixels {
image-rendering: crisp-edges;
/* older firefox browsers */
image-rendering: pixelated;
}
/* Layout */
.container {
width: 80%;
margin-left: auto;
margin-right: auto;
max-width: 720px;
}
/* Singletons */
#logo {
display: block;
width: 35%;
height: 35%;
color: rgba(0, 0, 0, 0);
/* make text transparent */
background: url('../icons/org.freedesktop.dabrain34.GstPipelineStudio.ico') no-repeat center;
padding: 5rem 0 3rem;
margin: 0 auto;
}
.page-logo>img {
margin: 0 auto;
}
@media (prefers-color-scheme: dark) {
a.page-logo {
filter: invert(100%) hue-rotate(180deg);
/* uninvert */
background-image: url('assets/page-logo-i.svg');
}
}
.brand-white {
background-color: #fff;
}
.brand-green {
background-color: #30D475;
}
.brand-black {
background-color: #201A26;
color: white;
}
.page-link::after {
content: " ➜";
}
/* Footer */
footer {
text-align: center;
padding: 3em 0 3em;
font-size: 1em;
margin-top: 4rem;
}
/* Make tables vertically aligned to the top */
tbody td {
vertical-align: top;
}
/* Github Code Highlighting */
.highlight table td {
padding: 5px;
}
.highlight table pre {
margin: 0;
}
.highlight .cm {
color: #999988;
font-style: italic;
}
.highlight .cp {
color: #999999;
font-weight: bold;
}
.highlight .c1 {
color: #999988;
font-style: italic;
}
.highlight .cs {
color: #999999;
font-weight: bold;
font-style: italic;
}
.highlight .c,
.highlight .ch,
.highlight .cd,
.highlight .cpf {
color: #999988;
font-style: italic;
}
.highlight .err {
color: #a61717;
background-color: #e3d2d2;
}
.highlight .gd {
color: #000000;
background-color: #ffdddd;
}
.highlight .ge {
color: #000000;
font-style: italic;
}
.highlight .gr {
color: #aa0000;
}
.highlight .gh {
color: #999999;
}
.highlight .gi {
color: #000000;
background-color: #ddffdd;
}
.highlight .go {
color: #888888;
}
.highlight .gp {
color: #555555;
}
.highlight .gs {
font-weight: bold;
}
.highlight .gu {
color: #aaaaaa;
}
.highlight .gt {
color: #aa0000;
}
.highlight .kc {
color: #000000;
font-weight: bold;
}
.highlight .kd {
color: #000000;
font-weight: bold;
}
.highlight .kn {
color: #000000;
font-weight: bold;
}
.highlight .kp {
color: #000000;
font-weight: bold;
}
.highlight .kr {
color: #000000;
font-weight: bold;
}
.highlight .kt {
color: #445588;
font-weight: bold;
}
.highlight .k,
.highlight .kv {
color: #000000;
font-weight: bold;
}
.highlight .mf {
color: #009999;
}
.highlight .mh {
color: #009999;
}
.highlight .il {
color: #009999;
}
.highlight .mi {
color: #009999;
}
.highlight .mo {
color: #009999;
}
.highlight .m,
.highlight .mb,
.highlight .mx {
color: #009999;
}
.highlight .sb {
color: #d14;
}
.highlight .sc {
color: #d14;
}
.highlight .sd {
color: #d14;
}
.highlight .s2 {
color: #d14;
}
.highlight .se {
color: #d14;
}
.highlight .sh {
color: #d14;
}
.highlight .si {
color: #d14;
}
.highlight .sx {
color: #d14;
}
.highlight .sr {
color: #009926;
}
.highlight .s1 {
color: #d14;
}
.highlight .ss {
color: #990073;
}
.highlight .s,
.highlight .sa,
.highlight .dl {
color: #d14;
}
.highlight .na {
color: #008080;
}
.highlight .bp {
color: #999999;
}
.highlight .nb {
color: #0086B3;
}
.highlight .nc {
color: #445588;
font-weight: bold;
}
.highlight .no {
color: #008080;
}
.highlight .nd {
color: #3c5d5d;
font-weight: bold;
}
.highlight .ni {
color: #800080;
}
.highlight .ne {
color: #990000;
font-weight: bold;
}
.highlight .nf,
.highlight .fm {
color: #990000;
font-weight: bold;
}
.highlight .nl {
color: #990000;
font-weight: bold;
}
.highlight .nn {
color: #555555;
}
.highlight .nt {
color: #000080;
}
.highlight .vc {
color: #008080;
}
.highlight .vg {
color: #008080;
}
.highlight .vi {
color: #008080;
}
.highlight .nv,
.highlight .vm {
color: #008080;
}
.highlight .ow {
color: #000000;
font-weight: bold;
}
.highlight .o {
color: #000000;
font-weight: bold;
}
.highlight .w {
color: #bbbbbb;
}
.highlight {
background-color: #f8f8f8;
}
/* Code Blocks */
.highlighter-rouge {
padding: 2px 1rem;
border-radius: 5px;
background-color: white;
overflow: auto;
}
.highlighter-rouge * {
background-color: white;
}
/* Inline Code */
code.highlighter-rouge {
padding: 2px 6px;
background-color: rgba(0, 0, 0, 0.07);
}
/* Buttons */
.dialog-buttons {
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: space-between;
margin-top: 6rem;
}
.inline-button {
display: inline-block;
font-weight: 900;
font-size: 90%;
padding: .4rem 1rem;
border-radius: var(--rounded-corner);
background-color: rgba(0, 0, 0, 0.05);
color: var(--dark5);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View file

@ -15,7 +15,7 @@
<caption>Composition</caption>
</screenshot>
</screenshots>
<url type="homepage">https://gitlab.freedesktop.org/dabrain34/GstPipelineStudio</url>
<url type="homepage">https://dabrain34.pages.freedesktop.org/GstPipelineStudio</url>
<url type="bugtracker">https://gitlab.freedesktop.org/dabrain34/GstPipelineStudio/issues</url>
<categories>
<category>Audio</category>
@ -27,7 +27,17 @@
<translation type="gettext">@gettext-package@</translation>
<launchable type="desktop-id">@app-id@.desktop</launchable>
<releases>
<release version="@version@" date="@current_date@"/>
<release version="@version@" date="@current_date@">
<description>
<p>Welcome to GstPipelineStudio</p>
<ul>
<li>logs: receive multiple log sources such as GST logs and messages.</li>
<li>settings: add a log level selection</li>
<li>rename gst_pipeline_studio to gst-pipeline-studio</li>
<li>can open a pipeline from the command line</li>
</ul>
</description>
</release>
</releases>
<content_rating type="oars-1.1">
<content_attribute id="violence-cartoon">none</content_attribute>

View file

@ -3,7 +3,7 @@ Name=GstPipelineStudio
GenericName=GPS
Comment=A GUI for GStreamer
Type=Application
Exec=gst_pipeline_studio
Exec=gst-pipeline-studio
Terminal=false
Categories=AudioVideo;Audio;Video;Midi;Settings;GNOME;GTK;
Icon=@icon@

Binary file not shown.

Before

Width:  |  Height:  |  Size: 267 KiB

After

Width:  |  Height:  |  Size: 210 KiB

53
index.html Normal file
View file

@ -0,0 +1,53 @@
<html>
< <head>
<title>GstPipelineStudio</title>
<link rel="stylesheet" href="data/css/style.css">
<link rel="icon" href="data/icons/org.freedesktop.dabrain34.GstPipelineStudio.ico">
</head>
<body>
<div class="container">
<h1 id="logo"></h1>
<h1>GstPipelineStudio: Draw your own pipeline ...</h1>
<p><img src="data/screenshots/gps_screenshot.png" alt=""></p>
<p>GstPipelineStudio aims to provide a graphical user interface to the GStreamer framework. From a first
step in the framework with a simple pipeline to a complex pipeline debugging, the tool provides a
friendly interface to add elements to a pipeline and debug it.</p>
<h2>GstPipelineStudio 0.3.5 is out, checkout the <a
href="https://gitlab.freedesktop.org/dabrain34/GstPipelineStudio/-/blob/main/ChangeLog.md?ref_type=heads#anchor-033">
Release Notes</a> !
<br>
<br>
<p>Download:</p>
<ul>
<li><a href="https://flathub.org/apps/org.freedesktop.dabrain34.GstPipelineStudio">Flathub</a></li>
<li><a
href="https://gitlab.freedesktop.org/dabrain34/GstPipelineStudio/uploads/0ab4098a9385f21d72881d27530e6e27/GstPipelineStudio-0.3.5.msi">Windows
MSI</a>
</li>
<li><a
href="https://gitlab.freedesktop.org/dabrain34/GstPipelineStudio/uploads/d6d703809e3023de04e7ad1449fcb4aa/GstPipelineStudio-0.3.5.dmg">MacOS</a>
</li>
</ul>
<h2 id="getting-gps">Contributing to GstPipelineStudio</h2>
<p>If you want to get the the code from Gitlab, you can visit this <a
href="https://flathub.org/apps/org.freedesktop.dabrain34.GstPipelineStudio">webpage</a>
and follow the <a
href="https://flathub.org/apps/org.freedesktop.dabrain34.GstPipelineStudio/README.md">README</a>
to build your own version of GstPipelineStudio.
</p>
</h2>
</body>
<footer class="site-footer">
<p>&copy; GstPipelineStudio, 2023</p>
<p><a href="https://gitlab.freedesktop.org/dabrain34/GstPipelineStudio">Website source</a></p>
</footer>
</html>

17
installer/macos/brew_setup.sh Executable file
View file

@ -0,0 +1,17 @@
#!/bin/bash
HOMEBREW_NO_INSTALL_CLEANUP=1
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install pkg-config
# GTK4 support
brew install gtk4
# brew install cairo libxrandr libxi libxcursor libxdamage libxinerama
brew install npm
npm install -g appdmg
exit 0

186
installer/macos/deploy_macos.sh Executable file
View file

@ -0,0 +1,186 @@
#!/bin/bash
test_ok() {
$*
if [ $? != 0 ]; then
exit 1
fi
}
# dependency library:
# Make a .app file: https://gist.github.com/oubiwann/453744744da1141ccc542ff75b47e0cf
# Make a .dmg file: https://github.com/LinusU/node-appdmg
# Can't find library: https://www.jianshu.com/p/441a7553700f
BUILD_DIR=builddir
PROJECTDIR="$( cd "$(dirname "$0")/../" ; pwd -P )"
TARGETDIR="${PROJECTDIR}/${BUILD_DIR}/INSTALL_GPS"
VERSION="$(cat VERSION)"
export VERSION
echo "VERSION=$VERSION"
GSTREAMER_OPTS="
-Dforce_fallback_for=gstreamer-1.0,libffi,pcre2 \
-Dgstreamer-1.0:libav=disabled \
-Dgstreamer-1.0:examples=disabled \
-Dgstreamer-1.0:introspection=disabled \
-Dgstreamer-1.0:rtsp_server=disabled \
-Dgstreamer-1.0:devtools=disabled \
-Dgst-plugins-base:tests=disabled \
-Dgstreamer-1.0:tests=disabled \
-Dgst-plugins-bad:openexr=disabled -Dgstreamer-1.0:gst-examples=disabled \
-Dorc:gtk_doc=disabled \
-Dgstreamer-1.0:ges=disabled \
-Dgstreamer-1.0:python=disabled"
# rebuild app release version
rm -rf "${TARGETDIR}"
test_ok meson --prefix=$TARGETDIR --buildtype=release ${BUILD_DIR} ${GSTREAMER_OPTS}
test_ok ninja -C ${BUILD_DIR} install
# copy app data files to target dir
echo -n "Copy app data files......"
test_ok mkdir -p "${TARGETDIR}/etc/"
mkdir -p "${TARGETDIR}/lib/gstreamer-1.0"
mkdir -p "${TARGETDIR}/share/doc"
mkdir -p "${TARGETDIR}/share/themes"
mkdir -p "${TARGETDIR}/share/glib-2.0/schemas"
mkdir -p "${TARGETDIR}/share/licenses/GstPipelineStudio"
mkdir -p "${TARGETDIR}/share/icons/hicolor/scalable/apps"
echo "[done]"
function lib_dependency_copy
{
local target=$1
local folder=$2
lib_dir="$( cd "$( dirname "$1" )" >/dev/null 2>&1 && pwd )"
libraries="$(otool -L $target | grep "/*.*dylib" -o | xargs)"
for lib in $libraries; do
if [[ '/usr/lib/' != ${lib:0:9} && '/System/Library/' != ${lib:0:16} ]]; then
if [[ '@' == ${lib:0:1} ]]; then
if [[ '@loader_path' == ${lib:0:12} ]]; then
cp -n "${lib/@loader_path/$lib_dir}" $folder
else
echo "Unsupported path: $lib"
fi
else
if [[ $lib != $target ]]; then
cp -n $lib $folder
fi
fi
fi
done
}
function lib_dependency_analyze
{
# This function use otool to analyze library dependency.
# then copy the dependency libraries to destination path
local library_dir=$1
local targets_dir=$2
libraries="$(find $library_dir -name \*.dylib -o -name \*.so -type f)"
for lib in $libraries; do
lib_dependency_copy $lib $targets_dir
# otool -L $lib | grep "/usr/local/*.*dylib" -o | xargs -I{} cp -n "{}" "$targets_dir"
done
}
# copy app dependency library to target dir
echo -n "Copy app dependency library......"
lib_dependency_copy ${TARGETDIR}/bin/gst-pipeline-studio "${TARGETDIR}/bin"
lib_dependency_copy ${TARGETDIR}/lib/libgobject-2.0.0.dylib "${TARGETDIR}/bin"
lib_dependency_copy ${TARGETDIR}/lib/libsoup-2.4.1.dylib "${TARGETDIR}/bin"
lib_dependency_copy "${TARGETDIR}/bin/libgtk-4.1.dylib" "${TARGETDIR}/bin"
for file in ${TARGETDIR}/lib/gstreamer-1.0/*.dylib
do
echo "${file}"
lib_dependency_copy ${file} "${TARGETDIR}/lib/"
done
test_ok cp -f "${PROJECTDIR}/macos/mac_launcher.sh" "${TARGETDIR}/bin/launcher.sh"
# FIXME should build and install gtk4 instead of using homebrew
# copy GTK runtime dependencies resource
# echo -n "Copy GTK runtime resource......"
# cp -rf /usr/local/lib/gio "${TARGETDIR}/lib/"
# cp -rf /usr/local/lib/gtk-3.0 "${TARGETDIR}/lib/"
# cp -rf /usr/local/lib/gdk-pixbuf-2.0 "${TARGETDIR}/lib/"
# cp -rf /usr/local/lib/girepository-1.0 "${TARGETDIR}/lib/"
# cp -rf /usr/local/lib/libgda-5.0 "${TARGETDIR}/lib/"
# # Avoid override the latest locale file
cp -r /opt/homebrew/share/locale "${TARGETDIR}/share/"
cp -rf /opt/homebrew/share/icons "${TARGETDIR}/share/"
cp -rf /opt/homebrew/share/gtk4-0 "${TARGETDIR}/share/"
# cp -rf /usr/local/share/fontconfig "${TARGETDIR}/share/"
# cp -rf /usr/local/share/themes/Mac "${TARGETDIR}/share/themes/"
# cp -rf /usr/local/share/themes/Default "${TARGETDIR}/share/themes/"
# cp -rf /usr/local/share/gtksourceview-4 "${TARGETDIR}/share/"
# glib-compile-schemas /usr/local/share/glib-2.0/schemas
# cp -f /usr/local/share/glib-2.0/schemas/gschema* "${TARGETDIR}/share/glib-2.0/schemas"
# # find "${TARGETDIR}/bin" -type f -path '*.dll.a' -exec rm '{}' \;
lib_dependency_analyze ${TARGETDIR}/lib ${TARGETDIR}/bin
lib_dependency_analyze ${TARGETDIR}/bin ${TARGETDIR}/bin
echo "[done]"
# copy app icons and license files to target dir
echo -n "Copy app icon(svg) files......"
cp -f "${PROJECTDIR}/../data/icons/org.freedesktop.dabrain34.GstPipelineStudio.ico" "${TARGETDIR}/bin"
cp -f "${PROJECTDIR}/../data/icons/org.freedesktop.dabrain34.GstPipelineStudio.svg" "${TARGETDIR}/share/icons/hicolor/scalable/apps"
echo "[done]"
# download license file: LGPL-3.0
echo -n "Downloading the remote license file......"
cp -f "${PROJECTDIR}/../LICENSE" "${TARGETDIR}/share/licenses/GstPipelineStudio"
if [ ! -f "${TARGETDIR}/share/licenses/GstPipelineStudio/gpl-3.0.txt" ]; then
curl "https://www.gnu.org/licenses/gpl-3.0.txt" -o "${TARGETDIR}/share/licenses/GstPipelineStudio/gpl-3.0.txt"
if [ $? -eq 0 ]; then
echo "[done]"
else
echo "[failed]"
fi
else
echo "[done]"
fi
echo "make macos executable file(.app)......"
cd "${PROJECTDIR}/${BUILD_DIR}"
cp "${PROJECTDIR}/macos/installers/Info.plist" "${PROJECTDIR}/${BUILD_DIR}"
cp "${PROJECTDIR}/macos/installers/mac.icns" "${PROJECTDIR}/${BUILD_DIR}/GstPipelineStudio.icns"
../macos/mac_app_pack.sh --path "${TARGETDIR}" --name "GstPipelineStudio" --info "Info.plist" --icons "GstPipelineStudio.icns"
if [ $? -eq 0 ]; then
echo "[done]"
else
echo "[failed]"
fi
# make installer package
echo "make macos installer(.dmg)......"
cp "${PROJECTDIR}/macos/installers/dmg.json" gps_dmg.json
cp "${PROJECTDIR}/macos/installers/background.png" "${PROJECTDIR}/${BUILD_DIR}/gps_dmg_background.png"
rm -f ${PROJECTDIR}/GstPipelineStudio-${VERSION}.dmg
appdmg gps_dmg.json "${PROJECTDIR}/GstPipelineStudio-${VERSION}.dmg"
if [ $? -eq 0 ]; then
echo "[done]"
else
echo "[failed]"
fi
# make portable package
echo -n "make macos portable......"
tar czf "${PROJECTDIR}/GstPipelineStudio-${VERSION}.tar.gz" -C "${PROJECTDIR}" ${BUILD_DIR}/GstPipelineStudio.app
if [ $? -eq 0 ]; then
echo "[done]"
else
echo "[failed]"
fi

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<!-- Reference: https://github.com/geany/geany-osx/blob/master/Info.plist -->
<dict>
<key>CFBundleShortVersionString</key>
<string>1.36</string>
<key>CFBundleVersion</key>
<string>1.36</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright (C)2022 Stephane Cerveau</string>
<key>CFBundleGetInfoString</key>
<string>Copyright (C)2022 Stephane Cerveau</string>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleName</key>
<string>GstPipelineStudio</string>
<key>GtkOSXLaunchScriptFile</key>
<string>launcher.sh</string>
<key>CFBundleExecutable</key>
<string>gst-pipeline-studio</string>
<key>CFBundleIconFile</key>
<string>GstPipelineStudio.icns</string>
<key>CFBundleIdentifier</key>
<string>org.freedesktop.dabrain34.GstPipelineStudio</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>LSMinimumSystemVersion</key>
<string>10.9</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.developer-tools</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

View file

@ -0,0 +1,9 @@
{
"title": "GstPipelineStudio installer",
"icon": "GstPipelineStudio.icns",
"background": "gps_dmg_background.png",
"contents": [
{ "x": 948, "y": 270, "type": "link", "path": "/Applications" },
{ "x": 692, "y": 270, "type": "file", "path": "GstPipelineStudio.app" }
]
}

Binary file not shown.

131
installer/macos/mac_app_pack.sh Executable file
View file

@ -0,0 +1,131 @@
#!/usr/bin/env bash
VERSION=4.0.1
SCRIPT=`basename "$0"`
APP_NAME="GstPipelineStudio"
APP_ICONS="/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericApplicationIcon.icns"
OSX_VERSION=`sw_vers -productVersion`
PWD=`pwd`
function usage {
cat <<EOF
$SCRIPT v${VERSION} for for Mac OS X
Usage:
$SCRIPT [options]
Options:
-h, --help Prints this help message, then exits
-p, --path Name of the path to 'appify' (required)
-n, --name Name of the application (default "$APP_NAME")
-o, --info Name of the Info.plist
-i, --icons Name of the icons file to use when creating the app
(defaults to $APP_ICONS)
-v, --version Prints the version of this script, then exits
Description:
Creates the GTK Mac app from a GTK install path.
Appify has one required parameter, the path to appify:
$SCRIPT --path gtk-app-install-path
Note that you cannot rename appified apps. If you want to give your app
a custom name, use the '--name' option
$SCRIPT --path gtk-app-install-path --name "Sweet"
Copyright:
Copyright (c) Thomas Aylott <http://subtlegradient.com/>
Modified by Mathias Bynens <http://mathiasbynens.be/>
Modified by Andrew Dvorak <http://OhReally.net/>
Rewritten by Duncan McGreggor <http://github.com/oubiwann/>
Modified by Zuhong Tao <https://github.com/taozuhong>
EOF
exit 1
}
function version {
echo "v${VERSION}"
exit 1
}
function error {
echo
echo "ERROR: $1"
echo
usage
}
while :; do
case $1 in
-h | --help ) usage;;
-p | --path ) APP_BUILD="$2"; shift ;;
-n | --name ) APP_NAME="$2"; shift ;;
-o | --info ) APP_INFO="$2"; shift ;;
-i | --icons ) APP_ICONS="$2"; shift ;;
-v | --version ) version;;
-- ) shift; break ;;
* ) break ;;
esac
shift
done
if [ -z ${APP_BUILD+nil} ]; then
error "The GTK app path to appify must be provided!"
fi
if [ ! -d "$APP_BUILD" ]; then
error "Can't find the target path '$APP_BUILD'"
fi
if [ -a "$APP_NAME.app" ]; then
rm -rf "$APP_NAME.app"
fi
SCRIPT_PATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
APP_OUT_DIR="$( cd "$(dirname "$APP_BUILD")" >/dev/null 2>&1 ; pwd -P )"
APP_TOP_DIR=$APP_OUT_DIR/$APP_NAME.app
APP_CON_DIR=$APP_TOP_DIR/Contents
APP_RES_DIR=$APP_CON_DIR/Resources
APP_EXE_DIR=$APP_CON_DIR/MacOS
APP_ETC_DIR=$APP_RES_DIR/etc
APP_LIB_DIR=$APP_RES_DIR/lib
# Copy GstPipelineStudio.app resource
mkdir -vp "$APP_CON_DIR"/{MacOS,Resources}
cp -f "$APP_INFO" "$APP_CON_DIR/Info.plist"
cp -f "$APP_ICONS" "$APP_RES_DIR/$APP_NAME.icns"
cp -rf "$APP_BUILD/etc" "$APP_RES_DIR"
cp -rf "$APP_BUILD/lib" "$APP_RES_DIR"
cp -rf "$APP_BUILD/share" "$APP_RES_DIR"
cp -rf "$APP_BUILD/libexec" "$APP_RES_DIR"
cp $APP_BUILD/bin/gst-pipeline-studio $APP_EXE_DIR/gst-pipeline-studio-real
cp $APP_BUILD/bin/launcher.sh $APP_EXE_DIR/gst-pipeline-studio
chmod 766 "$APP_EXE_DIR/gst-pipeline-studio"
chmod 766 "$APP_EXE_DIR/gst-pipeline-studio-real"
chmod -R 766 "$APP_RES_DIR"/libexec/gstreamer-1.0
# Copy dependency libraries and update their search path
source $SCRIPT_PATH/mac_app_path.sh
if ls $APP_BUILD/bin/*.so 1> /dev/null 2>&1; then
for sofile in $APP_BUILD/bin/*.so; do
cp $sofile $APP_LIB_DIR
done
fi
cp $APP_BUILD/bin/*.dylib $APP_LIB_DIR
chmod -R 766 $APP_LIB_DIR
echo -n "relocate the gstreamer plugins......"
for file in $APP_LIB_DIR/gstreamer-1.0/*.dylib
do
echo "relocating ${file}"
lib_change_paths \
@executable_path/../Resources/lib \
$APP_LIB_DIR \
${file}
done
lib_change_paths \
@executable_path/../Resources/lib \
$APP_LIB_DIR \
$APP_EXE_DIR/gst-pipeline-studio-real
lib_change_siblings $APP_LIB_DIR @loader_path
echo "Mac app bundled at '$APP_TOP_DIR'"

119
installer/macos/mac_app_path.sh Executable file
View file

@ -0,0 +1,119 @@
# SPDX-License-Identifier: GPL-2.0-or-later
# https://gitlab.com/inkscape/inkscape/-/blob/master/packaging/macos/bash_d/lib_.sh
# examples:
# lib_change_path \
# @executable_path/../Resources/lib/lib2geom.1.0.0.dylib \
# $APP_LIB_DIR/inkscape/libinkscape_base.dylib \
# $APP_EXE_DIR/inkscape
#
#lib_change_paths \
# @executable_path/../lib \
# $APP_LIB_DIR \
# $APP_BIN_DIR/gs
#
# lib_change_siblings $APP_LIB_DIR dylib
# lib_change_siblings $APP_LIB_DIR so
# include_guard
### includes ###################################################################
# Nothing here.
### variables ##################################################################
# Nothing here.
### functions ##################################################################
function lib_change_path
{
# This is a simple wrapper around install_name_tool to reduce the
# number of arguments (like $source does not have to be provided
# here as it can be deducted from $target).
# Also, the requested change can be applied to multiple binaries
# at once since 2-n arguments can be supplied.
local target=$1 # new path to dynamically linked library
local binaries=${*:2} # binaries to modify
local source_lib=${target##*/} # get library filename from target location
for binary in $binaries; do # won't work with spaces in paths
if [[ $binary == *.so ]] || [[ $binary == *.dylib ]]; then
lib_reset_id $binary
fi
local source=$(otool -L $binary | grep $source_lib | awk '{ print $1 }')
install_name_tool -change $source $target $binary
done
}
function lib_change_paths
{
# This is a slightly more advanced wrapper around install_name_tool.
# Given a directory $lib_dir that contains the libraries, all libraries
# linked in $binary can be changed at once to a specified $target path.
local target=$1 # new path to dynamically linked library
local lib_dir=$2
local binaries=${*:3}
for binary in $binaries; do
if [[ $binary == *.so ]] || [[ $binary == *.dylib ]]; then
lib_reset_id $binary
fi
for linked_lib in $(otool -L $binary | tail -n +2 | awk '{ print $1 }'); do
if [ "$(basename $binary)" != "$(basename $linked_lib)" ] &&
[ -f $lib_dir/$(basename $linked_lib) ]; then
lib_change_path $target/$(basename $linked_lib) $binary
fi
done
done
}
function lib_change_siblings
{
# This is a slightly more advanced wrapper around install_name_tool.
# All libraries inside a given $dir that are linked to libraries present
# in that $dir can be automatically adjusted.
local dir=$1
local target=$2
for lib in $dir/*.dylib; do
lib_reset_id $lib
for linked_lib in $(otool -L $lib | tail -n +2 | awk '{ print $1 }'); do
if [ "$(basename $lib)" != "$(basename $linked_lib)" ] &&
[ -f $dir/$(basename $linked_lib) ]; then
lib_change_path $target/$(basename $linked_lib) $lib
fi
done
done
if ls $dir/*.so 1> /dev/null 2>&1; then
for lib in $dir/*.so; do
lib_reset_id $lib
for linked_lib in $(otool -L $lib | tail -n +2 | awk '{ print $1 }'); do
if [ "$(basename $lib)" != "$(basename $linked_lib)" ] &&
[ -f $dir/$(basename $linked_lib) ]; then
lib_change_path $target/$(basename $linked_lib) $lib
fi
done
done
fi
}
function lib_reset_id
{
local lib=$1
install_name_tool -id $(basename $lib) $lib
}
### aliases ####################################################################
# Nothing here.
### main #######################################################################
# Nothing here.

170
installer/macos/mac_launcher.sh Executable file
View file

@ -0,0 +1,170 @@
#!/bin/sh
if test "x$GTK_DEBUG_LAUNCHER" != x; then
set -x
fi
if test "x$GTK_DEBUG_GDB" != x; then
EXEC="gdb --args"
else
EXEC=exec
fi
name=`basename "$0"`
bundle_app="$( cd "$( dirname "$0" )/../.." >/dev/null 2>&1 && pwd )"
bundle_contents="$bundle_app"/Contents
bundle_res="$bundle_contents"/Resources
bundle_lib="$bundle_res"/lib
bundle_libexec="$bundle_res"/libexec
bundle_bin="$bundle_res"/bin
bundle_data="$bundle_res"/share
bundle_etc="$bundle_res"/etc
export DYLD_LIBRARY_PATH="$bundle_lib"
export XDG_CONFIG_DIRS="$bundle_etc"/xdg
export XDG_DATA_DIRS="$bundle_data"
export GTK_DATA_PREFIX="$bundle_res"
export GTK_EXE_PREFIX="$bundle_res"
export GTK_PATH="$bundle_res"
export GST_PLUGIN_SCANNER="$bundle_libexec/gstreamer-1.0/gst-plugin-scanner"
export GIO_EXTRA_MODULES="$bundle_lib/gio/modules"
# GIO modules
export GIO_MODULE_DIR="$bundle_lib/gio/modules"
#GStreamer plugins
export GST_PLUGIN_PATH="$bundle_lib/gstreamer-1.0"
APP=$name
I18NDIR="$bundle_data/locale"
# Set the locale-related variables appropriately:
unset LANG LC_MESSAGES LC_MONETARY LC_COLLATE
# Has a language ordering been set?
# If so, set LC_MESSAGES and LANG accordingly; otherwise skip it.
# First step uses sed to clean off the quotes and commas, to change - to _, and change the names for the chinese scripts from "Hans" to CN and "Hant" to TW.
APPLELANGUAGES=`defaults read .GlobalPreferences AppleLanguages | sed -En -e 's/\-/_/' -e 's/Hant/TW/' -e 's/Hans/CN/' -e 's/[[:space:]]*\"?([[:alnum:]_]+)\"?,?/\1/p' `
if test "$APPLELANGUAGES"; then
# A language ordering exists.
# Test, item per item, to see whether there is an corresponding locale.
for L in $APPLELANGUAGES; do
#test for exact matches:
if test -f "$I18NDIR/${L}/LC_MESSAGES/$APP.mo"; then
export LANG=$L
break
fi
#This is a special case, because often the original strings are in US
#English and there is no translation file.
if test "x$L" == "xen_US"; then
export LANG=$L
break
fi
#OK, now test for just the first two letters:
if test -f "$I18NDIR/${L:0:2}/LC_MESSAGES/$APP.mo"; then
export LANG=${L:0:2}
break
fi
#Same thing, but checking for any english variant.
if test "x${L:0:2}" == "xen"; then
export LANG=$L
break
fi;
done
fi
unset APPLELANGUAGES L
# If we didn't get a language from the language list, try the Collation preference, in case it's the only setting that exists.
APPLECOLLATION=`defaults read .GlobalPreferences AppleCollationOrder`
if test -z ${LANG} -a -n $APPLECOLLATION; then
if test -f "$I18NDIR/${APPLECOLLATION:0:2}/LC_MESSAGES/$APP.mo"; then
export LANG=${APPLECOLLATION:0:2}
fi
fi
if test ! -z $APPLECOLLATION; then
export LC_COLLATE=$APPLECOLLATION
fi
unset APPLECOLLATION
# Continue by attempting to find the Locale preference.
APPLELOCALE=`defaults read .GlobalPreferences AppleLocale`
if test -f "$I18NDIR/${APPLELOCALE:0:5}/LC_MESSAGES/$APP.mo"; then
if test -z $LANG; then
export LANG="${APPLELOCALE:0:5}"
fi
elif test -z $LANG -a -f "$I18NDIR/${APPLELOCALE:0:2}/LC_MESSAGES/$APP.mo"; then
export LANG="${APPLELOCALE:0:2}"
fi
#Next we need to set LC_MESSAGES. If at all possible, we want a full
#5-character locale to avoid the "Locale not supported by C library"
#warning from Gtk -- even though Gtk will translate with a
#two-character code.
if test -n $LANG; then
#If the language code matches the applelocale, then that's the message
#locale; otherwise, if it's longer than two characters, then it's
#probably a good message locale and we'll go with it.
if test $LANG == ${APPLELOCALE:0:5} -o $LANG != ${LANG:0:2}; then
export LC_MESSAGES=$LANG
#Next try if the Applelocale is longer than 2 chars and the language
#bit matches $LANG
elif test $LANG == ${APPLELOCALE:0:2} -a $APPLELOCALE > ${APPLELOCALE:0:2}; then
export LC_MESSAGES=${APPLELOCALE:0:5}
#Fail. Get a list of the locales in $PREFIX/share/locale that match
#our two letter language code and pick the first one, special casing
#english to set en_US
elif test $LANG == "en"; then
export LC_MESSAGES="en_US"
else
LOC=`find $PREFIX/share/locale -name $LANG???`
for L in $LOC; do
export LC_MESSAGES=$L
done
fi
else
#All efforts have failed, so default to US english
export LANG="en_US"
export LC_MESSAGES="en_US"
fi
CURRENCY=`echo $APPLELOCALE | sed -En 's/.*currency=([[:alpha:]]+).*/\1/p'`
if test "x$CURRENCY" != "x"; then
#The user has set a special currency. Gtk doesn't install LC_MONETARY files, but Apple does in /usr/share/locale, so we're going to look there for a locale to set LC_CURRENCY to.
if test -f /usr/local/share/$LC_MESSAGES/LC_MONETARY; then
if test -a `cat /usr/local/share/$LC_MESSAGES/LC_MONETARY` == $CURRENCY; then
export LC_MONETARY=$LC_MESSAGES
fi
fi
if test -z "$LC_MONETARY"; then
FILES=`find /usr/share/locale -name LC_MONETARY -exec grep -H $CURRENCY {} \;`
if test -n "$FILES"; then
export LC_MONETARY=`echo $FILES | sed -En 's%/usr/share/locale/([[:alpha:]_]+)/LC_MONETARY.*%\1%p'`
fi
fi
fi
#No currency value means that the AppleLocale governs:
if test -z "$LC_MONETARY"; then
LC_MONETARY=${APPLELOCALE:0:5}
fi
#For Gtk, which only looks at LC_ALL:
export LC_ALL=$LC_MESSAGES
unset APPLELOCALE FILES LOC
if test -f "$bundle_lib/charset.alias"; then
export CHARSETALIASDIR="$bundle_lib"
fi
# Extra arguments can be added in environment.sh.
EXTRA_ARGS=
if test -f "$bundle_res/environment.sh"; then
source "$bundle_res/environment.sh"
fi
# Strip out the argument added by the OS.
if /bin/expr "x$1" : '^x-psn_' > /dev/null; then
shift 1
fi
$EXEC "$bundle_contents/MacOS/gst-pipeline-studio-real" "$@" $EXTRA_ARGS

View file

@ -78,7 +78,7 @@ This is a tricky question. We believe software patents should not exist, so that
\par \pard\plain \s0\nowidctlpar{\*\hyphen2\hyphlead2\hyphtrail2\hyphmax0}\cf0\kerning1\hich\af7\langfe2052\dbch\af8\afs24\alang1081\loch\f3\fs24\lang1031\sl276\slmult1\sb0\sa200{\b0\rtlch \ltrch\loch\fs24\lang255\loch\f6
Software patents are widely available in the USA. Despite they are formally prohibited in the European Union, they indeed are granted by the thousand by the European Patent Office, and also some national patent offices follow the same path. In other countries they are not available.}
\par \pard\plain \s0\nowidctlpar{\*\hyphen2\hyphlead2\hyphtrail2\hyphmax0}\cf0\kerning1\hich\af7\langfe2052\dbch\af8\afs24\alang1081\loch\f3\fs24\lang1031\sl276\slmult1\sb0\sa200{\b0\rtlch \ltrch\loch\fs24\lang255\loch\f6
Since patent protection is a national state-granted monopoly, distributing software that violates patents in a given country could be entirely safe if done in another country. Fair use exceptions also exist. So we cannot advice you whether the software we provide would be considered violating patents in your country or in any other country, but that can be said for virtually all kinds of sofware. Only, since we deal with audio-video standards, and these standards are by and large designed to use certain patented technologies, it is common wisdom that the pieces of software that implement these standards are sensitive in this respect.}
Since patent protection is a national state-granted monopoly, distributing software that violates patents in a given country could be entirely safe if done in another country. Fair use exceptions also exist. So we cannot advice you whether the software we provide would be considered violating patents in your country or in any other country, but that can be said for virtually all kinds of software. Only, since we deal with audio-video standards, and these standards are by and large designed to use certain patented technologies, it is common wisdom that the pieces of software that implement these standards are sensitive in this respect.}
\par \pard\plain \s0\nowidctlpar{\*\hyphen2\hyphlead2\hyphtrail2\hyphmax0}\cf0\kerning1\hich\af7\langfe2052\dbch\af8\afs24\alang1081\loch\f3\fs24\lang1031\sl276\slmult1\sb0\sa200{\b0\rtlch \ltrch\loch\fs24\lang255\loch\f6
This is why GStreamer has taken a modular approach, so that you can use a Free plugins or a proprietary, patent royalty bearing, plugin for a given standard.}
\par \pard\plain \s0\nowidctlpar{\*\hyphen2\hyphlead2\hyphtrail2\hyphmax0}\cf0\kerning1\hich\af7\langfe2052\dbch\af8\afs24\alang1081\loch\f3\fs24\lang1031\sl276\slmult1\sb0\sa200{\b\rtlch \ltrch\loch\fs36\lang255\loch\f6

View file

@ -1,12 +1,23 @@
$wixFolder = Join-Path $PSScriptRoot -ChildPath 'wix/'
# Check wix installation
$wixInstalledFolder = "C:\Program Files (x86)\WiX Toolset v3.11\bin"
if(Test-Path $wixInstalledFolder)
{
$wixFolder = $wixInstalledFolder
}
else
{
$prepareWix = Join-Path $PSScriptRoot -ChildPath prepare_wix.ps1
& "$prepareWix"
$wixFolder = Join-Path $PSScriptRoot -ChildPath 'wix/'
}
$candleToolPath = Join-Path $wixFolder -ChildPath candle.exe
$lightToolPath = Join-Path $wixFolder -ChildPath light.exe
$heatToolPath = Join-Path $wixFolder -ChildPath heat.exe
$GPSUpgradeCode = "9B87C8FF-599C-4F20-914E-AF5E68CB3DC0"
$GPSVersion = $(git describe --always --abbrev=0)
Write-Output $GPSVersion
$GPSVersion = "0.2.3"
$GPSUpgradeCode = "9B87C8FF-599C-4F20-914E-AF5E68CB3DC0"
$GPSVersion = Get-Content $PSScriptRoot\..\..\VERSION -Raw
Write-Output $GPSVersion
try
{
Push-Location $PSScriptRoot
@ -22,13 +33,15 @@ try
# GST and GTK are installed in this folder by prepare_gstreamer.ps1.
# GST and GTK are built by the docker image.
$gstreamerInstallDir="c:\gst-install-clean"
$gstreamerBinInstallDir= Join-Path $gstreamerInstallDir -ChildPath "bin/"
$gstreamerPluginInstallDir= Join-Path $gstreamerInstallDir -ChildPath "lib\gstreamer-1.0"
$gstreamerBinInstallDir= Join-Path $gstreamerInstallDir -ChildPath "bin"
$gstreamerPluginInstallDir= Join-Path $gstreamerInstallDir -ChildPath "lib"
$gstreamerShareInstallDir= Join-Path $gstreamerInstallDir -ChildPath "share"
& "$heatToolPath" dir "$gstreamerBinInstallDir" -gg -sfrag -template:fragment -out gstreamer-1.0.wxs -cg "_gstreamer" -var var.gstreamerBinInstallDir -dr INSTALLFOLDER
& "$heatToolPath" dir "$gstreamerPluginInstallDir" -gg -sfrag -template:fragment -out gstreamer-plugins-1.0.wxs -cg "_gstreamer_plugins" -var var.gstreamerPluginInstallDir -dr INSTALLFOLDER
& "$heatToolPath" dir "$gstreamerShareInstallDir" -v -ke -gg -sfrag -template:fragment -out gstreamer-share-1.0.wxs -cg "_gstreamer_share" -var var.gstreamerShareInstallDir -dr INSTALLFOLDER
$files = "gps gstreamer-1.0 gstreamer-plugins-1.0"
$files = "gps gstreamer-1.0 gstreamer-plugins-1.0 gstreamer-share-1.0"
$wxs_files = @()
$obj_files = @()
foreach ($f in $files.split(" ")){
@ -40,13 +53,13 @@ try
# compiling wxs file into wixobj
$msiFileName = "GstPipelineStudio-$GPSVersion.msi"
foreach ($f in $wxs_files){
& "$candleToolPath" "$f" -dPlatform=x64 -dGPSUpgradeCode="$GPSUpgradeCode" -dGPSVersion="$GPSVersion" -dgstreamerBinInstallDir="$gstreamerBinInstallDir" -dgstreamerPluginInstallDir="$gstreamerPluginInstallDir"
& "$candleToolPath" "$f" -dPlatform=x64 -dGPSUpgradeCode="$GPSUpgradeCode" -dGPSVersion="$GPSVersion" -dgstreamerBinInstallDir="$gstreamerBinInstallDir" -dgstreamerPluginInstallDir="$gstreamerPluginInstallDir" -dgstreamerShareInstallDir="$gstreamerShareInstallDir"
if($LASTEXITCODE -ne 0)
{
throw "Compilation of $wxsFileName failed with exit code $LASTEXITCODE"
}
}
$AllArgs = $obj_files + @('-out', $msiFileName)
& $lightToolPath $AllArgs -ext WixUIExtension

View file

@ -3,6 +3,6 @@ set MYDIR=%~dp0
setlocal
set PATH=%MYDIR%bin;%PATH%
echo %PATH%
set GST_PLUGIN_PATH=%MYDIR%/gstreamer-1.0
set GST_PLUGIN_PATH=%MYDIR%\lib\gstreamer-1.0
echo %GST_PLUGIN_PATH%
gst_pipeline_studio.exe
gst-pipeline-studio.exe

View file

@ -10,10 +10,12 @@
<Package InstallScope="perMachine" Compressed="yes" />
<MajorUpgrade DowngradeErrorMessage="A later version of [ProductName] is already installed. Setup will now exit." />
<WixVariable Id="WixUIBannerBmp" Value="wixbanner.bmp" />
<WixVariable Id="WixUIDialogBmp" Value="wixdialog.bmp" />
<MediaTemplate EmbedCab="yes" />
<UIRef Id="WixUI_Mondo" />
<UIRef Id="WixUI_InstallDir" />
<WixVariable Id="WixUILicenseRtf" Value="LICENSE.rtf" />
<Directory Id="TARGETDIR" Name="SourceDir">
@ -59,6 +61,7 @@
<ComponentRef Id="ProductComponent" />
<ComponentGroupRef Id="_gstreamer" />
<ComponentGroupRef Id="_gstreamer_plugins" />
<ComponentGroupRef Id="_gstreamer_share" />
<ComponentRef Id="UninstallShortcut" />
</Feature>

View file

@ -9,3 +9,4 @@ Copy-Item -Path C:\gst-install\bin\*.exe -Destination c:\gst-install-clean\bin\
New-Item c:\gst-install-clean\lib\gstreamer-1.0 -ItemType Directory
Copy-Item -Path C:\gst-install\lib\gstreamer-1.0\*.dll -Destination c:\gst-install-clean\lib\gstreamer-1.0
Copy-Item -Path C:\gst-install\share -Destination c:\gst-install-clean\ -Recurse

BIN
installer/wix/wixbanner.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

BIN
installer/wix/wixdialog.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 KiB

View file

@ -1,23 +1,25 @@
project('gst_pipeline_studio',
version: '0.2.3',
meson_version: '>= 0.50.0',
project('gst-pipeline-studio',
version: '0.3.5',
meson_version: '>= 0.63.0',
default_options: [ 'warning_level=2',
],
license: 'GPL-3.0-or-later',
)
host_system = host_machine.system()
python_mod = import('python')
python3 = python_mod.find_installation()
current_date = run_command(python3, '-c', 'import datetime; print(datetime.datetime.now().strftime("%Y-%m-%d"), end ="")').stdout()
i18n = import('i18n')
host_system = host_machine.system()
dependency('gstreamer-1.0', version: '>= 1.20', fallback: ['gstreamer-1.0'])
dependency('gstreamer-base-1.0', version: '>= 1.20', fallback: ['gstreamer-1.0'])
dependency('gstreamer-video-1.0', version: '>= 1.20', fallback: ['gstreamer-1.0'])
dependency('glib-2.0', version: '>= 2.66')
dependency('gio-2.0', version: '>= 2.66')
dependency('gtk4', version: '>= 4.0.0')
dependency('gstreamer-1.0', version: '>= 1.18')
dependency('gstreamer-base-1.0', version: '>= 1.18')
dependency('gstreamer-plugins-base-1.0', version: '>= 1.18')
dependency('gstreamer-plugins-bad-1.0', version: '>= 1.18')
dependency('gtk4', version: '>= 4.0.0', fallback: ['gtk'])
find_program('cargo', required: true)
find_program('glib-compile-resources', required: true)
@ -34,7 +36,7 @@ localedir = prefix / get_option('localedir')
datadir = prefix / get_option('datadir')
pkgdatadir = datadir / meson.project_name()
iconsdir = datadir / 'icons'
podir = meson.source_root () / 'po'
podir = meson.project_source_root () / 'po'
gettext_package = meson.project_name()
base_id = 'org.freedesktop.dabrain34.GstPipelineStudio'
@ -68,8 +70,8 @@ subdir('po')
meson.add_dist_script(
'build-aux/dist-vendor.sh',
meson.source_root(),
join_paths(meson.build_root(), 'meson-dist', meson.project_name() + '-' + version)
meson.project_source_root(),
join_paths(meson.project_build_root(), 'meson-dist', meson.project_name() + '-' + version)
)
if host_system == 'linux'
meson.add_install_script('build-aux/meson/postinstall.py')

View file

@ -3,13 +3,22 @@
- Update to the given version:
- meson.build
- cargo.toml
- VERSION
- index.html
- And rebuild to regenerate the cargo.lock
- update the changelog in org.freedesktop.dabrain34.GstPipelineStudio.appdata.xml.in.in within release/description
- create a tag on gitlab
- meson builddir -Dbuildtype=release
- ninja -C buiddir dist
- upload the package and the sha256 to gitlab for Flatpak in the release notes
- Fetch the package from the `linux release` job or you can make it manually with:
- meson builddir -Dbuildtype=release
- ninja -C builddir dist
- Upload the package .xz file and the sha256 to gitlab release page in the release notes
see https://gitlab.freedesktop.org/dabrain34/GstPipelineStudio/-/releases/0.3.2
# flathub
https://github.com/flathub/org.freedesktop.dabrain34.GstPipelineStudio
- Need to update the package and the sha256 generated by ninja -C builddir dist
- Need to update the package and the sha256 from the release page, ie https://gitlab.freedesktop.org/dabrain34/GstPipelineStudio/-/releases/0.3.2
- Create a pull request with the package update
- Wait at lest 2-3 hours after merging to get the update available.

View file

@ -7,17 +7,16 @@
// SPDX-License-Identifier: GPL-3.0-only
use glib::SignalHandlerId;
use glib::Value;
use gtk::gdk;
use gtk::prelude::*;
use gtk::{gio, gio::SimpleAction, glib, graphene};
use gtk::{
Application, ApplicationWindow, Builder, Button, FileChooserAction, FileChooserDialog, Paned,
PopoverMenu, ResponseType, Statusbar, Viewport, Widget,
PopoverMenu, ResponseType, Statusbar, Widget,
};
use log::error;
use once_cell::unsync::OnceCell;
use std::cell::RefCell;
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Write};
@ -25,11 +24,12 @@ use std::ops;
use std::rc::{Rc, Weak};
use crate::gps as GPS;
use crate::graphbook;
use crate::logger;
use crate::settings::Settings;
use crate::ui as GPSUI;
use crate::{GPS_DEBUG, GPS_ERROR, GPS_INFO, GPS_TRACE, GPS_WARN};
use crate::{GPS_DEBUG, GPS_ERROR, GPS_TRACE, GPS_WARN};
use crate::graphmanager as GM;
use crate::graphmanager::PropertyExt;
@ -37,9 +37,9 @@ use std::fmt;
#[derive(Debug)]
pub struct GPSAppInner {
pub window: gtk::ApplicationWindow,
pub graphview: RefCell<GM::GraphView>,
pub current_graphtab: Cell<u32>,
pub graphbook: RefCell<HashMap<u32, graphbook::GraphTab>>,
pub builder: Builder,
pub player: RefCell<GPS::Player>,
pub plugin_list_initialized: OnceCell<bool>,
pub signal_handlers: RefCell<HashMap<String, SignalHandlerId>>,
}
@ -95,27 +95,24 @@ impl GPSApp {
window.set_application(Some(application));
window.set_title(Some("GStreamer Pipeline Studio"));
let player = GPS::Player::new().expect("Unable to initialize GStreamer subsystem");
let app = GPSApp(Rc::new(GPSAppInner {
window,
graphview: RefCell::new(GM::GraphView::new()),
current_graphtab: Cell::new(0),
graphbook: RefCell::new(HashMap::new()),
builder,
player: RefCell::new(player),
plugin_list_initialized: OnceCell::new(),
signal_handlers: RefCell::new(HashMap::new()),
}));
let app_weak = app.downgrade();
app.player.borrow().set_app(app_weak);
app.graphview.borrow_mut().set_id(0);
let settings = Settings::load_settings();
app.window
.set_default_size(settings.app_width, settings.app_height);
#[cfg(not(target_os = "macos"))]
if settings.app_maximized {
app.window.maximize();
} else {
app.window
.set_size_request(settings.app_width, settings.app_height);
}
app.set_paned_position(&settings, "graph_dashboard-paned", 100);
app.set_paned_position(&settings, "graph_logs-paned", 100);
app.set_paned_position(&settings, "elements_preview-paned", 100);
@ -153,7 +150,7 @@ impl GPSApp {
.insert(paned_name.to_string(), paned.position());
}
pub fn on_startup(application: &gtk::Application) {
pub fn on_startup(application: &gtk::Application, pipeline_desc: &String) {
// Create application and error out if that fails for whatever reason
let app = match GPSApp::new(application) {
Ok(app) => app,
@ -163,13 +160,7 @@ impl GPSApp {
}
};
// When the application is activated show the UI. This happens when the first process is
// started, and in the first process whenever a second process is started
let app_weak = app.downgrade();
application.connect_activate(glib::clone!(@weak application => move |_| {
let app = upgrade_weak!(app_weak);
app.build_ui(&application);
}));
app.build_ui(application, pipeline_desc);
let app_weak = app.downgrade();
let slider: gtk::Scale = app
@ -178,18 +169,20 @@ impl GPSApp {
.expect("Couldn't get status_bar");
let slider_update_signal_id = slider.connect_value_changed(move |slider| {
let app = upgrade_weak!(app_weak);
let player = app.player.borrow();
let value = slider.value() as u64;
GPS_TRACE!("Seeking to {} s", value);
if player.set_position(value).is_err() {
if graphbook::current_graphtab(&app)
.player()
.set_position(value)
.is_err()
{
GPS_ERROR!("Seeking to {} failed", value);
}
});
let app_weak = app.downgrade();
let timeout_id =
glib::timeout_add_local(std::time::Duration::from_millis(500), move || {
let app = upgrade_weak!(app_weak, glib::Continue(false));
let player = app.player.borrow();
let app = upgrade_weak!(app_weak, glib::ControlFlow::Break);
let label: gtk::Label = app
.builder
@ -199,19 +192,21 @@ impl GPSApp {
.builder
.object("scale-position")
.expect("Couldn't get status_bar");
let position = player.position();
let duration = player.duration();
let position = graphbook::current_graphtab(&app).player().position();
let duration = graphbook::current_graphtab(&app).player().duration();
slider.set_range(0.0, duration as f64 / 1000_f64);
slider.block_signal(&slider_update_signal_id);
slider.set_value(position as f64 / 1000_f64);
slider.unblock_signal(&slider_update_signal_id);
// Query the current playing position from the underlying player.
let position_desc = player.position_description();
let position_desc = graphbook::current_graphtab(&app)
.player()
.position_description();
// Display the playing position in the gui.
label.set_text(&position_desc);
// Tell the callback to continue calling this closure.
glib::Continue(true)
glib::ControlFlow::Continue
});
let timeout_id = RefCell::new(Some(timeout_id));
@ -228,8 +223,8 @@ impl GPSApp {
.expect("Couldn't get the main window");
let mut settings = Settings::load_settings();
settings.app_maximized = window.is_maximized();
settings.app_width = window.width();
settings.app_height = window.height();
settings.app_width = window.default_width();
settings.app_height = window.default_height();
app.save_paned_position(&mut settings, "graph_dashboard-paned");
app.save_paned_position(&mut settings, "graph_logs-paned");
app.save_paned_position(&mut settings, "elements_preview-paned");
@ -262,7 +257,9 @@ impl GPSApp {
application.set_accels_for_action("app.open_pipeline", &["<primary>p"]);
application.add_action(&gio::SimpleAction::new("save_as", None));
application.add_action(&gio::SimpleAction::new("save", None));
application.set_accels_for_action("app.save", &["<primary>s"]);
application.add_action(&gio::SimpleAction::new("save_as", None));
application.add_action(&gio::SimpleAction::new("delete", None));
application.set_accels_for_action("app.delete", &["<primary>d", "Delete"]);
@ -278,6 +275,7 @@ impl GPSApp {
application.add_action(&gio::SimpleAction::new("logger.clear", None));
application.add_action(&gio::SimpleAction::new("graph.check", None));
application.add_action(&gio::SimpleAction::new("graph.clear", None));
application.add_action(&gio::SimpleAction::new("graph.pipeline_details", None));
application.add_action(&gio::SimpleAction::new("port.delete", None));
@ -331,7 +329,7 @@ impl GPSApp {
.expect("Unable to dynamic cast to SimpleAction")
}
fn disconnect_app_menu_action(&self, action_name: &str) {
pub fn disconnect_app_menu_action(&self, action_name: &str) {
let action = self.app_menu_action(action_name);
if let Some(signal_handler_id) = self.signal_handlers.borrow_mut().remove(action_name) {
@ -420,7 +418,12 @@ impl GPSApp {
.builder
.object("notebook-preview")
.expect("Couldn't get box_preview");
if n_sink == 0 {
let mut n_video_sink = n_sink;
for tab in self.graphbook.borrow().values() {
n_video_sink += tab.player().n_video_sink();
}
if n_video_sink == 0 {
loop {
let i = notebook_preview.n_pages();
if i == 0 {
@ -435,32 +438,35 @@ impl GPSApp {
.valign(gtk::Align::Center)
.build();
box_preview.append(&picture);
let label = gtk::Label::new(Some(&format!("Preview{n_sink}")));
let label = gtk::Label::new(Some(&format!("Preview{n_video_sink}")));
notebook_preview.insert_page(&box_preview, Some(&label), None);
notebook_preview.set_current_page(Some(n_video_sink as u32));
}
pub fn build_ui(&self, application: &Application) {
let drawing_area_window: Viewport = self
.builder
.object("drawing_area")
.expect("Couldn't get drawing_area");
drawing_area_window.set_child(Some(&*self.graphview.borrow()));
pub fn build_ui(&self, application: &Application, pipeline_desc: &String) {
graphbook::setup_graphbook(self);
graphbook::create_graphtab(self, 0, None);
let (ready_tx, ready_rx) = async_channel::unbounded::<(logger::LogType, String)>();
// Setup the logger to get messages into the TreeView
let (ready_tx, ready_rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
let app_weak = self.downgrade();
logger::init_logger(
ready_tx,
Settings::default_log_file_path()
ready_tx.clone(),
Settings::log_file_path()
.to_str()
.expect("Unable to convert log file path to a string"),
);
GPSUI::logger::setup_logger_list(self);
let _ = ready_rx.attach(None, move |msg: String| {
let app = upgrade_weak!(app_weak, glib::Continue(false));
GPSUI::logger::add_to_logger_list(&app, &msg);
glib::Continue(true)
logger::init_msg_logger(ready_tx);
GPSUI::logger::setup_logger_list(self, "treeview-app-logger", logger::LogType::App);
GPSUI::logger::setup_logger_list(self, "treeview-msg-logger", logger::LogType::Message);
GPSUI::logger::setup_logger_list(self, "treeview-gst-logger", logger::LogType::Gst);
let app_weak = self.downgrade();
glib::spawn_future_local(async move {
while let Ok(msg) = ready_rx.recv().await {
let app = upgrade_weak!(app_weak, glib::ControlFlow::Break);
GPSUI::logger::add_to_logger_list(&app, msg.0, &msg.1);
}
glib::ControlFlow::Continue
});
let window = &self.window;
@ -478,15 +484,20 @@ impl GPSApp {
let app_weak = self.downgrade();
self.connect_app_menu_action("new-window", move |_, _| {
let app = upgrade_weak!(app_weak);
app.clear_graph();
GPS_ERROR!("clear graph");
let id = graphbook::graphbook_get_new_graphtab_id(&app);
graphbook::create_graphtab(&app, id, None);
let graphbook: gtk::Notebook = app
.builder
.object("graphbook")
.expect("Couldn't get graphbook");
graphbook.set_current_page(Some(id));
});
let app_weak = self.downgrade();
self.connect_app_menu_action("open", move |_, _| {
let app = upgrade_weak!(app_weak);
GPSApp::get_file_from_dialog(&app, false, move |app, filename| {
app.load_graph(&filename)
app.load_graph(&filename, false)
.unwrap_or_else(|_| GPS_ERROR!("Unable to open file {}", filename));
});
});
@ -500,12 +511,31 @@ impl GPSApp {
&Settings::recent_pipeline_description(),
&app,
move |app, pipeline_desc| {
app.load_pipeline(&pipeline_desc)
.unwrap_or_else(|_| GPS_ERROR!("Unable to open file {}", pipeline_desc));
app.load_pipeline(&pipeline_desc).unwrap_or_else(|_| {
GPS_ERROR!("Unable to open pipeline description {}", pipeline_desc)
});
Settings::set_recent_pipeline_description(&pipeline_desc);
},
);
});
let app_weak = self.downgrade();
self.connect_app_menu_action("save", move |_, _| {
let app = upgrade_weak!(app_weak);
let gt = graphbook::current_graphtab(&app);
if gt.undefined() {
GPSApp::get_file_from_dialog(&app, true, move |app, filename| {
GPS_DEBUG!("Save file {}", filename);
app.save_graph(&filename)
.unwrap_or_else(|_| GPS_ERROR!("Unable to save file to {}", filename));
graphbook::current_graphtab_set_filename(&app, filename.as_str());
});
} else if gt.modified() {
let filename = gt.filename();
app.save_graph(&filename)
.unwrap_or_else(|_| GPS_ERROR!("Unable to save file to {}", filename));
graphbook::current_graphtab_set_filename(&app, filename.as_str());
}
});
let app_weak = self.downgrade();
self.connect_app_menu_action("save_as", move |_, _| {
@ -514,6 +544,7 @@ impl GPSApp {
GPS_DEBUG!("Save file {}", filename);
app.save_graph(&filename)
.unwrap_or_else(|_| GPS_ERROR!("Unable to save file to {}", filename));
graphbook::current_graphtab_set_filename(&app, filename.as_str());
});
});
@ -526,8 +557,9 @@ impl GPSApp {
let app_weak = self.downgrade();
self.connect_app_menu_action("delete", move |_, _| {
let app = upgrade_weak!(app_weak);
let graph_view = app.graphview.borrow();
graph_view.delete_selected();
graphbook::current_graphtab(&app)
.graphview()
.delete_selected();
});
let app_weak = self.downgrade();
@ -539,28 +571,28 @@ impl GPSApp {
let app_weak = self.downgrade();
self.connect_button_action("button-play", move |_| {
let app = upgrade_weak!(app_weak);
let graph_view = app.graphview.borrow();
let _ = app
.player
.borrow()
.start_pipeline(&graph_view, GPS::PipelineState::Playing);
let _ = graphbook::current_graphtab(&app).player().start_pipeline(
&graphbook::current_graphtab(&app).graphview(),
GPS::PipelineState::Playing,
);
});
let app_weak = self.downgrade();
self.connect_button_action("button-pause", move |_| {
let app = upgrade_weak!(app_weak);
let graph_view = app.graphview.borrow();
let _ = app
.player
.borrow()
.start_pipeline(&graph_view, GPS::PipelineState::Paused);
let _ = graphbook::current_graphtab(&app).player().start_pipeline(
&graphbook::current_graphtab(&app).graphview(),
GPS::PipelineState::Paused,
);
});
let app_weak = self.downgrade();
self.connect_button_action("button-stop", move |_| {
let app = upgrade_weak!(app_weak);
let player = app.player.borrow();
let _ = player.set_state(GPS::PipelineState::Stopped);
let _ = graphbook::current_graphtab(&app)
.player()
.set_state(GPS::PipelineState::Stopped);
});
let app_weak = self.downgrade();
@ -568,271 +600,27 @@ impl GPSApp {
let app = upgrade_weak!(app_weak);
app.clear_graph();
});
let app_weak = self.downgrade();
self.graphview.borrow().connect_local(
"node-added",
false,
glib::clone!(@weak application => @default-return None, move |values: &[Value]| {
let app = upgrade_weak!(app_weak, None);
let graph_id = values[1].get::<u32>().expect("graph id in args[1]");
let node_id = values[2].get::<u32>().expect("node id in args[2]");
GPS_INFO!("Node added node id={} in graph id={}", node_id, graph_id);
if let Some(node) = app.graphview.borrow().node(node_id) {
let description = GPS::ElementInfo::element_description(&node.name()).ok();
node.set_tooltip_markup(description.as_deref());
for port in node.all_ports(GM::PortDirection::All) {
let caps = PropertyExt::property(&port,"_caps");
GPS_TRACE!("caps={} for port id {}", caps.clone().unwrap_or_else(|| "caps unknown".to_string()), port.id());
port.set_tooltip_markup(caps.as_deref());
}
}
None
}),
);
let app_weak = self.downgrade();
self.graphview.borrow().connect_local(
"port-added",
false,
glib::clone!(@weak application => @default-return None, move |values: &[Value]| {
let app = upgrade_weak!(app_weak, None);
let graph_id = values[1].get::<u32>().expect("graph id in args[1]");
let node_id = values[2].get::<u32>().expect("node id in args[2]");
let port_id = values[3].get::<u32>().expect("port id in args[3]");
GPS_INFO!("Port added port id={} to node id={} in graph id={}", port_id, node_id, graph_id);
if let Some(node) = app.graphview.borrow().node(node_id) {
if let Some(port) = node.port(port_id) {
let caps = PropertyExt::property(&port, "_caps");
GPS_TRACE!("caps={} for port id {}", caps.clone().unwrap_or_else(|| "caps unknown".to_string()), port.id());
port.set_tooltip_markup(caps.as_deref());
}
}
None
}),
);
let app_weak = self.downgrade();
self.graphview.borrow().connect_local(
"graph-updated",
false,
glib::clone!(@weak application => @default-return None, move |values: &[Value]| {
let app = upgrade_weak!(app_weak, None);
let id = values[1].get::<u32>().expect("id in args[1]");
GPS_TRACE!("Graph updated id={}", id);
let _ = app
.save_graph(
Settings::default_graph_file_path()
.to_str()
.expect("Unable to convert to string"),
)
.map_err(|e| GPS_WARN!("Unable to save file {}", e));
None
}),
);
// When user clicks on port with right button
let app_weak = self.downgrade();
self.graphview
.borrow()
.connect_local(
"graph-right-clicked",
false,
glib::clone!(@weak application => @default-return None, move |values: &[Value]| {
let app = upgrade_weak!(app_weak, None);
let point = values[1].get::<graphene::Point>().expect("point in args[2]");
let pop_menu = app.app_pop_menu_at_position(&*app.graphview.borrow(), point.to_vec2().x() as f64, point.to_vec2().y() as f64);
let menu: gio::MenuModel = app
.builder
.object("graph_menu")
.expect("Couldn't graph_menu");
pop_menu.set_menu_model(Some(&menu));
let app_weak = app.downgrade();
app.connect_app_menu_action("graph.check",
move |_,_| {
let app = upgrade_weak!(app_weak);
let pipeline_description = app.player.borrow().pipeline_description_from_graphview(&app.graphview.borrow());
if app.player.borrow().create_pipeline(&pipeline_description).is_ok() {
GPSUI::message::display_message_dialog(&pipeline_description,gtk::MessageType::Info, |_| {});
} else {
GPSUI::message::display_error_dialog(false, &format!("Unable to render:\n\n{pipeline_description}"));
}
}
);
let app_weak = app.downgrade();
app.connect_app_menu_action("graph.pipeline_details",
move |_,_| {
let app = upgrade_weak!(app_weak);
GPSUI::properties::display_pipeline_details(&app);
}
);
pop_menu.show();
None
}),
);
// When user clicks on port with right button
let app_weak = self.downgrade();
self.graphview.borrow().connect_local(
"port-right-clicked",
false,
move |values: &[Value]| {
let app = upgrade_weak!(app_weak, None);
let port_id = values[1].get::<u32>().expect("port id args[1]");
let node_id = values[2].get::<u32>().expect("node id args[2]");
let point = values[3]
.get::<graphene::Point>()
.expect("point in args[3]");
let pop_menu = app.app_pop_menu_at_position(
&*app.graphview.borrow(),
point.to_vec2().x() as f64,
point.to_vec2().y() as f64,
);
let menu: gio::MenuModel = app
.builder
.object("port_menu")
.expect("Couldn't get menu model for port");
pop_menu.set_menu_model(Some(&menu));
if app.graphview.borrow().can_remove_port(node_id, port_id) {
let app_weak = app.downgrade();
app.connect_app_menu_action("port.delete", move |_, _| {
let app = upgrade_weak!(app_weak);
GPS_TRACE!("port.delete-link port {} node {}", port_id, node_id);
app.graphview.borrow().remove_port(node_id, port_id);
});
} else {
app.disconnect_app_menu_action("port.delete");
}
let app_weak = app.downgrade();
app.connect_app_menu_action("port.properties", move |_, _| {
let app = upgrade_weak!(app_weak);
GPS_TRACE!("port.properties port {} node {}", port_id, node_id);
let node = app.node(node_id);
let port = app.port(node_id, port_id);
GPSUI::properties::display_pad_properties(
&app,
&node.name(),
&port.name(),
node_id,
port_id,
);
});
pop_menu.show();
None
},
);
// When user clicks on node with right button
let app_weak = self.downgrade();
self.graphview
.borrow()
.connect_local(
"node-right-clicked",
false,
glib::clone!(@weak application => @default-return None, move |values: &[Value]| {
let app = upgrade_weak!(app_weak, None);
let node_id = values[1].get::<u32>().expect("node id args[1]");
let point = values[2].get::<graphene::Point>().expect("point in args[2]");
let pop_menu = app.app_pop_menu_at_position(&*app.graphview.borrow(), point.to_vec2().x() as f64, point.to_vec2().y() as f64);
let menu: gio::MenuModel = app
.builder
.object("node_menu")
.expect("Couldn't get menu model for node");
pop_menu.set_menu_model(Some(&menu));
let app_weak = app.downgrade();
app.connect_app_menu_action("node.add-to-favorite",
move |_,_| {
let app = upgrade_weak!(app_weak);
GPS_DEBUG!("node.add-to-favorite {}", node_id);
if let Some(node) = app.graphview.borrow().node(node_id) {
GPSUI::elements::add_to_favorite_list(&app, node.name());
};
}
);
let app_weak = app.downgrade();
app.connect_app_menu_action("node.delete",
move |_,_| {
let app = upgrade_weak!(app_weak);
GPS_DEBUG!("node.delete {}", node_id);
app.graphview.borrow_mut().remove_node(node_id);
}
);
let node = app.node(node_id);
if let Some(input) = GPS::ElementInfo::element_supports_new_pad_request(&node.name(), GM::PortDirection::Input) {
let app_weak = app.downgrade();
app.connect_app_menu_action("node.request-pad-input",
move |_,_| {
let app = upgrade_weak!(app_weak);
GPS_DEBUG!("node.request-pad-input {}", node_id);
app.create_port_with_caps(node_id, GM::PortDirection::Input, GM::PortPresence::Sometimes, input.caps().to_string());
}
);
} else {
app.disconnect_app_menu_action("node.request-pad-input");
}
let node = app.node(node_id);
if let Some(output) = GPS::ElementInfo::element_supports_new_pad_request(&node.name(), GM::PortDirection::Output) {
let app_weak = app.downgrade();
app.connect_app_menu_action("node.request-pad-output",
move |_,_| {
let app = upgrade_weak!(app_weak);
GPS_DEBUG!("node.request-pad-output {}", node_id);
app.create_port_with_caps(node_id, GM::PortDirection::Output, GM::PortPresence::Sometimes, output.caps().to_string());
}
);
} else {
app.disconnect_app_menu_action("node.request-pad-output");
}
let app_weak = app.downgrade();
app.connect_app_menu_action("node.properties",
move |_,_| {
let app = upgrade_weak!(app_weak);
GPS_DEBUG!("node.properties {}", node_id);
let node = app.graphview.borrow().node(node_id).unwrap();
GPSUI::properties::display_plugin_properties(&app, &node.name(), node_id);
}
);
pop_menu.show();
None
}),
);
let app_weak = self.downgrade();
self.graphview.borrow().connect_local(
"node-double-clicked",
false,
glib::clone!(@weak application => @default-return None, move |values: &[Value]| {
let app = upgrade_weak!(app_weak, None);
let node_id = values[1].get::<u32>().expect("node id args[1]");
GPS_TRACE!("Node double clicked id={}", node_id);
let node = app.graphview.borrow().node(node_id).unwrap();
GPSUI::properties::display_plugin_properties(&app, &node.name(), node_id);
None
}),
);
// Setup the favorite list
GPSUI::elements::setup_favorite_list(self);
// Setup the favorite list
GPSUI::elements::setup_elements_list(self);
let _ = self
.load_graph(
Settings::default_graph_file_path()
.to_str()
.expect("Unable to convert to string"),
)
.map_err(|_e| {
GPS_WARN!("Unable to load default graph");
if pipeline_desc.is_empty() {
let _ = self
.load_graph(
Settings::graph_file_path()
.to_str()
.expect("Unable to convert to string"),
true,
)
.map_err(|_e| {
GPS_WARN!("Unable to load default graph");
});
} else {
self.load_pipeline(pipeline_desc).unwrap_or_else(|_| {
GPS_ERROR!("Unable to open pipeline description {}", pipeline_desc)
});
}
}
// Downgrade to a weak reference
@ -844,25 +632,22 @@ impl GPSApp {
fn drop(self) {}
pub fn add_new_element(&self, element_name: &str) {
let graphview = self.graphview.borrow();
let (inputs, outputs) = GPS::PadInfo::pads(element_name, false);
let node =
graphview.create_node(element_name, GPS::ElementInfo::element_type(element_name));
let node = graphbook::current_graphtab(self)
.graphview()
.create_node(element_name, GPS::ElementInfo::element_type(element_name));
let node_id = node.id();
if GPS::ElementInfo::element_is_uri_src_handler(element_name) {
GPSApp::get_file_from_dialog(self, false, move |app, filename| {
GPS_DEBUG!("Open file {}", filename);
let graphview = app.graphview.borrow();
let mut properties: HashMap<String, String> = HashMap::new();
properties.insert(String::from("location"), filename);
if let Some(node) = graphview.node(node_id) {
if let Some(node) = graphbook::current_graphtab(&app).graphview().node(node_id) {
node.update_properties(&properties);
}
});
} else if GPS::ElementInfo::element_is_capsfilter(element_name) {
node.set_light(true);
}
graphview.add_node(node);
graphbook::current_graphtab(self).graphview().add_node(node);
for input in inputs {
self.create_port_with_caps(
node_id,
@ -881,16 +666,15 @@ impl GPSApp {
}
}
fn node(&self, node_id: u32) -> GM::Node {
let node = self
.graphview
.borrow()
pub fn node(&self, node_id: u32) -> GM::Node {
let node = graphbook::current_graphtab(self)
.graphview()
.node(node_id)
.unwrap_or_else(|| panic!("Unable to retrieve node with id {}", node_id));
node
}
fn port(&self, node_id: u32, port_id: u32) -> GM::Port {
pub fn port(&self, node_id: u32, port_id: u32) -> GM::Port {
let node = self.node(node_id);
node.port(port_id)
.unwrap_or_else(|| panic!("Unable to retrieve port with id {}", port_id))
@ -941,14 +725,17 @@ impl GPSApp {
GM::PortDirection::Output => String::from("src_"),
_ => String::from("?"),
};
let graphview = self.graphview.borrow();
let port_name = format!("{}{}", port_name, ports.len());
let port = graphview.create_port(&port_name, direction, presence);
let port = graphbook::current_graphtab(self)
.graphview()
.create_port(&port_name, direction, presence);
let id = port.id();
let properties: HashMap<String, String> = HashMap::from([("_caps".to_string(), caps)]);
port.update_properties(&properties);
if let Some(mut node) = graphview.node(node_id) {
graphview.add_port_to_node(&mut node, port);
if let Some(mut node) = graphbook::current_graphtab(self).graphview().node(node_id) {
graphbook::current_graphtab(self)
.graphview()
.add_port_to_node(&mut node, port);
}
id
}
@ -959,42 +746,45 @@ impl GPSApp {
node_to_id: u32,
port_from_id: u32,
port_to_id: u32,
active: bool,
) {
let graphview = self.graphview.borrow();
let graphtab = graphbook::current_graphtab(self);
let link =
graphview.create_link(node_from_id, node_to_id, port_from_id, port_to_id, active);
graphview.add_link(link);
graphtab
.graphview()
.create_link(node_from_id, node_to_id, port_from_id, port_to_id);
graphtab.graphview().add_link(link);
}
fn clear_graph(&self) {
let graph_view = self.graphview.borrow();
graph_view.clear();
graphbook::current_graphtab(self).graphview().clear();
}
fn save_graph(&self, filename: &str) -> anyhow::Result<()> {
let graph_view = self.graphview.borrow();
pub fn save_graph(&self, filename: &str) -> anyhow::Result<()> {
let mut file = File::create(filename)?;
let buffer = graph_view.render_xml()?;
let buffer = graphbook::current_graphtab(self).graphview().render_xml()?;
file.write_all(&buffer)?;
Ok(())
}
fn load_graph(&self, filename: &str) -> anyhow::Result<()> {
let graph_view = self.graphview.borrow();
GPS_INFO!("Open graph file {}", filename);
fn load_graph(&self, filename: &str, untitled: bool) -> anyhow::Result<()> {
let mut file = File::open(filename)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).expect("buffer overflow");
graph_view.load_from_xml(buffer)?;
let graphtab = graphbook::current_graphtab(self);
graphtab.graphview().load_from_xml(buffer)?;
if !untitled {
graphbook::current_graphtab_set_filename(self, filename);
}
Ok(())
}
fn load_pipeline(&self, pipeline_desc: &str) -> anyhow::Result<()> {
let player = self.player.borrow();
let graphview = self.graphview.borrow();
player.graphview_from_pipeline_description(&graphview, pipeline_desc);
let graphtab = graphbook::current_graphtab(self);
let pd_parsed = pipeline_desc.replace('\\', "");
graphtab
.player()
.graphview_from_pipeline_description(&graphtab.graphview(), &pd_parsed);
Ok(())
}
}

View file

@ -12,6 +12,10 @@ use gtk::glib;
pub fn init() -> Result<()> {
std::env::set_var("GST_XINITTHREADS", "1");
gst::init()?;
#[cfg(feature = "gtk4-plugin")]
{
gstgtk4::plugin_register_static().expect("Failed to register gstgtk4 plugin");
}
gtk::init()?;
Ok(())
}

View file

@ -1,3 +1,3 @@
// SPDX-License-Identifier: GPL-3.0-or-later
pub static APP_ID: &str = @APP_ID@;
pub static VERSION: &str = @VERSION@;
pub static VERSION: &str = env!("CARGO_PKG_VERSION");

View file

@ -46,6 +46,19 @@ impl ElementInfo {
Ok(elements)
}
pub fn element_factory_exists(element_name: &str) -> bool {
match ElementInfo::element_feature(element_name) {
Some(_feature) => {
GPS_DEBUG!("Found element factory name {}", element_name);
true
}
None => {
GPS_ERROR!("Unable to find element factory name {}", element_name);
false
}
}
}
pub fn element_feature(element_name: &str) -> Option<gst::PluginFeature> {
let registry = gst::Registry::get();
gst::Registry::find_feature(&registry, element_name, gst::ElementFactory::static_type())
@ -60,59 +73,68 @@ impl ElementInfo {
pub fn element_description(element_name: &str) -> anyhow::Result<String> {
let mut desc = String::from("");
let feature = ElementInfo::element_feature(element_name)
.ok_or_else(|| glib::bool_error!("Failed get element feature"))?;
let rank = feature.rank();
if let Ok(factory) = feature.downcast::<gst::ElementFactory>() {
if !ElementInfo::element_factory_exists(element_name) {
desc.push_str("<b>Factory details:</b>\n");
desc.push_str("<b>Rank:</b>");
let _ = write!(desc, "{rank:?}",);
desc.push('\n');
desc.push_str("<b>Name:</b>");
desc.push_str(&factory.name());
desc.push_str(element_name);
desc.push('\n');
desc.push('\n');
desc.push_str("Factory unavailable.");
} else {
let feature = ElementInfo::element_feature(element_name)
.ok_or_else(|| glib::bool_error!("Failed get element feature"))?;
let rank = feature.rank();
if let Ok(factory) = feature.downcast::<gst::ElementFactory>() {
desc.push_str("<b>Factory details:</b>\n");
desc.push_str("<b>Rank:</b>");
let _ = write!(desc, "{rank:?}",);
desc.push('\n');
desc.push_str("<b>Name:</b>");
desc.push_str(&factory.name());
desc.push('\n');
let element_keys = factory.metadata_keys();
for key in element_keys {
let val = factory.metadata(&key);
if let Some(val) = val {
desc.push_str("<b>");
desc.push_str(&key);
desc.push_str("</b>:");
desc.push_str(&gtk::glib::markup_escape_text(val));
let element_keys = factory.metadata_keys();
for key in element_keys {
let val = factory.metadata(&key);
if let Some(val) = val {
desc.push_str("<b>");
desc.push_str(&key);
desc.push_str("</b>:");
desc.push_str(&gtk::glib::markup_escape_text(val));
desc.push('\n');
}
}
let feature = factory.upcast::<gst::PluginFeature>();
let plugin = gst::PluginFeature::plugin(&feature);
if let Some(plugin) = plugin {
desc.push('\n');
desc.push_str("<b>Plugin details:</b>");
desc.push('\n');
desc.push_str("<b>Name:");
desc.push_str("</b>");
desc.push_str(gst::Plugin::plugin_name(&plugin).as_str());
desc.push('\n');
desc.push_str("<b>Description:");
desc.push_str("</b>");
desc.push_str(&gtk::glib::markup_escape_text(&plugin.description()));
desc.push('\n');
desc.push_str("<b>Filename:");
desc.push_str("</b>");
desc.push_str(&gtk::glib::markup_escape_text(
&plugin
.filename()
.unwrap_or_default()
.as_path()
.display()
.to_string(),
));
desc.push('\n');
desc.push_str("<b>Version:");
desc.push_str("</b>");
desc.push_str(&gtk::glib::markup_escape_text(&plugin.version()));
desc.push('\n');
}
}
let feature = factory.upcast::<gst::PluginFeature>();
let plugin = gst::PluginFeature::plugin(&feature);
if let Some(plugin) = plugin {
desc.push('\n');
desc.push_str("<b>Plugin details:</b>");
desc.push('\n');
desc.push_str("<b>Name:");
desc.push_str("</b>");
desc.push_str(gst::Plugin::plugin_name(&plugin).as_str());
desc.push('\n');
desc.push_str("<b>Description:");
desc.push_str("</b>");
desc.push_str(&gtk::glib::markup_escape_text(&plugin.description()));
desc.push('\n');
desc.push_str("<b>Filename:");
desc.push_str("</b>");
desc.push_str(&gtk::glib::markup_escape_text(
&plugin
.filename()
.unwrap_or_default()
.as_path()
.display()
.to_string(),
));
desc.push('\n');
desc.push_str("<b>Version:");
desc.push_str("</b>");
desc.push_str(&gtk::glib::markup_escape_text(&plugin.version()));
desc.push('\n');
}
}
Ok(desc)
}
@ -134,13 +156,31 @@ impl ElementInfo {
}
pub fn element_property(element: &gst::Element, property_name: &str) -> anyhow::Result<String> {
let value = element
.property_value(property_name)
.transform::<String>()
.expect("Unable to transform to string")
.get::<String>()
.unwrap_or_default();
Ok(value)
let value = element.property_value(property_name);
if value.type_().is_a(glib::Type::ENUM) {
let value = value.get::<&glib::EnumValue>().unwrap().nick().to_string();
Ok(value)
} else if value.type_().is_a(glib::Type::FLAGS) {
let value = value.get::<Vec<&glib::FlagsValue>>().unwrap();
let flags = value.iter().copied().fold(0, |acc, val| acc | val.value());
Ok(flags.to_string())
} else if value.type_().is_a(glib::Type::F64) || value.type_().is_a(glib::Type::F32) {
let value = value
.transform::<String>()
.expect("Unable to transform to string")
.get::<String>()
.unwrap()
.replace(',', ".");
Ok(value)
} else {
let value = value
.transform::<String>()
.expect("Unable to transform to string")
.get::<String>()
.unwrap_or_default()
.to_lowercase();
Ok(value)
}
}
pub fn element_property_by_feature_name(
@ -159,7 +199,7 @@ impl ElementInfo {
element: &gst::Element,
) -> anyhow::Result<HashMap<String, glib::ParamSpec>> {
let mut properties_list = HashMap::new();
let params = element.class().list_properties();
let params = element.list_properties();
for param in params.iter() {
GPS_INFO!("Property_name {}", param.name());
@ -213,10 +253,6 @@ impl ElementInfo {
}
}
pub fn element_is_capsfilter(element_name: &str) -> bool {
matches!(element_name, "capsfilter")
}
pub fn element_supports_new_pad_request(
element_name: &str,
direction: PortDirection,
@ -240,7 +276,7 @@ impl ElementInfo {
None
}
pub fn search_fo_element(bin: &gst::Bin, element_name: &str) -> Vec<gst::Element> {
pub fn search_for_element(bin: &gst::Bin, element_name: &str) -> Vec<gst::Element> {
let mut iter = bin.iterate_elements();
let mut elements: Vec<gst::Element> = Vec::new();
elements = loop {
@ -248,7 +284,7 @@ impl ElementInfo {
Ok(Some(element)) => {
if element.is::<gst::Bin>() {
let bin = element.dynamic_cast::<gst::Bin>().unwrap();
let mut bin_elements = ElementInfo::search_fo_element(&bin, element_name);
let mut bin_elements = ElementInfo::search_for_element(&bin, element_name);
elements.append(&mut bin_elements);
} else {
GPS_INFO!("Found factory: {}", element.factory().unwrap().name());

View file

@ -51,41 +51,43 @@ impl PadInfo {
}
pub fn pads(element_name: &str, include_on_request: bool) -> (Vec<PadInfo>, Vec<PadInfo>) {
let feature = ElementInfo::element_feature(element_name).expect("Unable to get feature");
let mut input = vec![];
let mut output = vec![];
if let Ok(factory) = feature.downcast::<gst::ElementFactory>() {
if factory.num_pad_templates() > 0 {
let pads = factory.static_pad_templates();
for pad in pads {
GPS_TRACE!("Found a pad name {}", pad.name_template());
if pad.presence() == gst::PadPresence::Always
|| (include_on_request
&& (pad.presence() == gst::PadPresence::Request
|| pad.presence() == gst::PadPresence::Sometimes))
{
if pad.direction() == gst::PadDirection::Src {
output.push(PadInfo {
name: Some(pad.name_template().to_string()),
element_name: Some(element_name.to_string()),
direction: PortDirection::Output,
presence: PadInfo::pad_to_port_presence(pad.presence()),
caps: Some(pad.caps().to_string()),
});
} else if pad.direction() == gst::PadDirection::Sink {
input.push(PadInfo {
name: Some(pad.name_template().to_string()),
element_name: Some(element_name.to_string()),
direction: PortDirection::Input,
presence: PadInfo::pad_to_port_presence(pad.presence()),
caps: Some(pad.caps().to_string()),
});
if let Some(feature) = ElementInfo::element_feature(element_name) {
if let Ok(factory) = feature.downcast::<gst::ElementFactory>() {
if factory.num_pad_templates() > 0 {
let pads = factory.static_pad_templates();
for pad in pads {
GPS_TRACE!("Found a pad name {}", pad.name_template());
if pad.presence() == gst::PadPresence::Always
|| (include_on_request
&& (pad.presence() == gst::PadPresence::Request
|| pad.presence() == gst::PadPresence::Sometimes))
{
if pad.direction() == gst::PadDirection::Src {
output.push(PadInfo {
name: Some(pad.name_template().to_string()),
element_name: Some(element_name.to_string()),
direction: PortDirection::Output,
presence: PadInfo::pad_to_port_presence(pad.presence()),
caps: Some(pad.caps().to_string()),
});
} else if pad.direction() == gst::PadDirection::Sink {
input.push(PadInfo {
name: Some(pad.name_template().to_string()),
element_name: Some(element_name.to_string()),
direction: PortDirection::Input,
presence: PadInfo::pad_to_port_presence(pad.presence()),
caps: Some(pad.caps().to_string()),
});
}
}
}
}
}
(input, output)
} else {
(input, output)
}
(input, output)
}
}

View file

@ -61,12 +61,35 @@ impl PlayerWeak {
}
}
fn gst_log_handler(
category: gst::DebugCategory,
level: gst::DebugLevel,
file: &glib::GStr,
function: &glib::GStr,
line: u32,
_obj: Option<&gst::LoggedObject>,
message: &gst::DebugMessage,
) {
let log_message = format!(
"{}\t{}\t{}:{}:{}\t{}",
level,
category.name(),
line,
file.as_str(),
function.as_str(),
message.get().unwrap().as_str()
);
GPS_GST_LOG!("{}", log_message);
}
#[derive(Debug)]
pub struct PlayerInner {
app: RefCell<Option<GPSApp>>,
pipeline: RefCell<Option<gst::Pipeline>>,
current_state: Cell<PipelineState>,
n_video_sink: Cell<usize>,
bus_watch_guard: RefCell<Option<gst::bus::BusWatchGuard>>,
}
impl Player {
@ -76,12 +99,9 @@ impl Player {
pipeline: RefCell::new(None),
current_state: Cell::new(PipelineState::Stopped),
n_video_sink: Cell::new(0),
bus_watch_guard: RefCell::new(None),
}));
#[cfg(feature = "gtk4-plugin")]
{
gstgtk4::plugin_register_static().expect("Failed to register gstgtk4 plugin");
}
gst::log::add_log_function(gst_log_handler);
Ok(pipeline)
}
@ -103,13 +123,13 @@ impl Player {
.parse::<bool>()
.expect("Should a boolean value")
{
ElementInfo::element_update_rank("gtk4paintablesink", gst::Rank::Primary);
ElementInfo::element_update_rank("gtk4paintablesink", gst::Rank::PRIMARY);
} else {
ElementInfo::element_update_rank("gtk4paintablesink", gst::Rank::Marginal);
ElementInfo::element_update_rank("gtk4paintablesink", gst::Rank::MARGINAL);
}
gst::log::set_threshold_from_string(settings::Settings::gst_log_level().as_str(), true);
// Create pipeline from the description
let pipeline = gst::parse_launch(description)?;
let pipeline = gst::parse::launch(description)?;
let pipeline = pipeline.downcast::<gst::Pipeline>();
/* start playing */
if pipeline.is_err() {
@ -121,20 +141,22 @@ impl Player {
self.check_for_gtk4sink(pipeline.as_ref().unwrap());
// GPSApp is not Send(trait) ready , so we use a channel to exchange the given data with the main thread and use
// GPSApp.
let (ready_tx, ready_rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
let (ready_tx, ready_rx) = async_channel::unbounded::<gst::Element>();
let player_weak = self.downgrade();
let _ = ready_rx.attach(None, move |element: gst::Element| {
let player = upgrade_weak!(player_weak, glib::Continue(false));
let paintable = element.property::<gdk::Paintable>("paintable");
let n_sink = player.n_video_sink.get();
player
.app
.borrow()
.as_ref()
.expect("App should be available")
.set_app_preview(&paintable, n_sink);
player.n_video_sink.set(n_sink + 1);
glib::Continue(true)
glib::spawn_future_local(async move {
while let Ok(element) = ready_rx.recv().await {
let player = upgrade_weak!(player_weak, glib::ControlFlow::Break);
let paintable = element.property::<gdk::Paintable>("paintable");
let n_sink = player.n_video_sink.get();
player
.app
.borrow()
.as_ref()
.expect("App should be available")
.set_app_preview(&paintable, n_sink);
player.n_video_sink.set(n_sink + 1);
}
glib::ControlFlow::Continue
});
let bin = pipeline.unwrap().dynamic_cast::<gst::Bin>();
if let Ok(bin) = bin.as_ref() {
@ -142,7 +164,7 @@ impl Player {
if let Some(factory) = element.factory() {
GPS_INFO!("Received the signal deep element added {}", factory.name());
if factory.name() == "gtk4paintablesink" {
let _ = ready_tx.send(element.clone());
let _ = ready_tx.try_send(element.clone());
}
}
});
@ -153,7 +175,7 @@ impl Player {
pub fn check_for_gtk4sink(&self, pipeline: &gst::Pipeline) {
let bin = pipeline.clone().dynamic_cast::<gst::Bin>().unwrap();
let gtksinks = ElementInfo::search_fo_element(&bin, "gtk4paintablesink");
let gtksinks = ElementInfo::search_for_element(&bin, "gtk4paintablesink");
for (first_sink, gtksink) in gtksinks.into_iter().enumerate() {
let paintable = gtksink.property::<gdk::Paintable>("paintable");
@ -180,12 +202,13 @@ impl Player {
let bus = pipeline.bus().expect("Pipeline had no bus");
let pipeline_weak = self.downgrade();
bus.add_watch_local(move |_bus, msg| {
let pipeline = upgrade_weak!(pipeline_weak, glib::Continue(false));
let bus_watch_guard = bus.add_watch_local(move |_bus, msg| {
let pipeline = upgrade_weak!(pipeline_weak, glib::ControlFlow::Break);
pipeline.on_pipeline_message(msg);
glib::Continue(true)
glib::ControlFlow::Continue
})?;
*self.pipeline.borrow_mut() = Some(pipeline);
*self.bus_watch_guard.borrow_mut() = Some(bus_watch_guard);
}
self.set_state(new_state).map_err(|error| {
@ -203,6 +226,7 @@ impl Player {
PipelineState::Paused => pipeline.set_state(gst::State::Paused)?,
PipelineState::Stopped | PipelineState::Error => {
pipeline.set_state(gst::State::Null)?;
self.n_video_sink.set(0);
gst::StateChangeSuccess::Success
}
};
@ -272,6 +296,9 @@ impl Player {
pub fn playing(&self) -> bool {
self.state() == PipelineState::Playing || self.state() == PipelineState::Paused
}
pub fn n_video_sink(&self) -> usize {
self.n_video_sink.get()
}
pub fn downgrade(&self) -> PlayerWeak {
PlayerWeak(Rc::downgrade(&self.0))
@ -279,6 +306,9 @@ impl Player {
fn on_pipeline_message(&self, msg: &gst::MessageRef) {
use gst::MessageView;
if let Some(message) = msg.structure() {
GPS_MSG_LOG!("{:?}", message);
}
match msg.view() {
MessageView::Eos(_) => {
GPS_INFO!("EOS received");
@ -317,7 +347,7 @@ impl Player {
.unwrap()
.dynamic_cast::<gst::Bin>()
.unwrap();
let elements_name: Vec<String> = ElementInfo::search_fo_element(&bin, "")
let elements_name: Vec<String> = ElementInfo::search_for_element(&bin, "")
.iter()
.map(|e| e.factory().unwrap().name().to_string())
.collect();
@ -362,6 +392,11 @@ impl Player {
if n_ports > 1 {
let _ = write!(description, "{unique_name}. ! ");
} else {
if let Some(link) = graphview.port_link(port.id()) {
if !link.name().is_empty() {
let _ = write!(description, "! {} ", link.name());
}
}
description.push_str("! ");
}
if let Some(node) = graphview.node(node_to) {
@ -416,7 +451,6 @@ impl Player {
peer_node.id(),
port.id(),
peer_port.id(),
true,
);
}
}
@ -525,15 +559,9 @@ impl Player {
impl Drop for PlayerInner {
fn drop(&mut self) {
// TODO: If a recording is currently running we would like to finish that first
// before quitting the pipeline and shutting down the pipeline.
if let Some(pipeline) = self.pipeline.borrow().to_owned() {
// We ignore any errors here
let _ = pipeline.set_state(gst::State::Null);
// Remove the message watch from the bus
let bus = pipeline.bus().expect("Pipeline had no bus");
let _ = bus.remove_watch();
}
}
}

499
src/graphbook.rs Normal file
View file

@ -0,0 +1,499 @@
// graphbook.rs
//
// Copyright 2021 Stéphane Cerveau <scerveau@collabora.com>
//
// This file is part of GstPipelineStudio
//
// SPDX-License-Identifier: GPL-3.0-only
use crate::app::{GPSApp, GPSAppWeak};
use crate::gps as GPS;
use crate::graphmanager as GM;
use crate::graphmanager::PropertyExt;
use crate::logger;
use crate::settings::Settings;
use crate::ui as GPSUI;
use crate::{GPS_DEBUG, GPS_TRACE, GPS_WARN};
use glib::Value;
use gtk::prelude::*;
use gtk::{gio, glib, graphene};
use std::cell::{Cell, Ref, RefCell};
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq)]
enum TabState {
Undefined = 0,
Modified,
Saved,
}
#[derive(Debug, Clone)]
pub struct GraphTab {
graphview: RefCell<GM::GraphView>,
player: RefCell<GPS::Player>,
id: Cell<u32>,
name: gtk::Label,
filename: RefCell<String>,
state: Cell<TabState>,
}
impl GraphTab {
pub fn new(app: GPSAppWeak, id: u32, filename: &str) -> Self {
let label = gtk::Label::new(Some("Untitled*"));
let graphtab = GraphTab {
id: Cell::new(id),
graphview: RefCell::new(GM::GraphView::new()),
player: RefCell::new(
GPS::Player::new().expect("Unable to initialize GStreamer subsystem"),
),
name: label,
filename: RefCell::new(filename.to_string()),
state: Cell::new(TabState::Undefined),
};
graphtab
.graphview
.borrow()
.set_id(graphbook_get_new_graphview_id(&app));
graphtab.player.borrow().set_app(app);
graphtab
}
pub fn id(&self) -> u32 {
self.id.get()
}
pub fn widget_label(&self) -> &gtk::Label {
&self.name
}
pub fn graphview(&self) -> Ref<GM::GraphView> {
self.graphview.borrow()
}
pub fn player(&self) -> Ref<GPS::Player> {
self.player.borrow()
}
pub fn set_name(&self, name: &str) {
self.name.set_text(name);
}
pub fn basename(&self) -> String {
Path::new(&self.filename.borrow().as_str())
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_string()
}
pub fn set_filename(&self, filename: &str) {
self.filename.replace(filename.to_string());
self.set_name(self.basename().as_str());
self.set_modified(false);
}
pub fn filename(&self) -> String {
self.filename.borrow().clone()
}
pub fn set_modified(&self, modified: bool) {
let name = self.basename();
if modified {
self.set_name(&(name + "*"));
self.state.set(TabState::Modified);
} else {
self.set_name(name.as_str());
self.state.set(TabState::Saved);
}
}
pub fn undefined(&self) -> bool {
self.state.get() == TabState::Undefined
}
pub fn modified(&self) -> bool {
self.state.get() == TabState::Modified
}
}
pub fn graphtab(app: &GPSApp, id: u32) -> GraphTab {
app.graphbook
.borrow()
.get(&id)
.cloned()
.expect("the current graphtab should be ok")
}
pub fn graphbook_get_new_graphview_id(app_weak: &GPSAppWeak) -> u32 {
let app = app_weak.upgrade();
let mut graphview_id: u32 = 0;
for tab in app.unwrap().graphbook.borrow().values() {
if tab.graphview().id() > graphview_id {
graphview_id = tab.graphview().id()
}
}
graphview_id + 1
}
pub fn graphbook_get_new_graphtab_id(app: &GPSApp) -> u32 {
let mut graphtab_id: u32 = 0;
for tab in app.graphbook.borrow().values() {
if tab.id() > graphtab_id {
graphtab_id = tab.id()
}
}
graphtab_id + 1
}
pub fn current_graphtab(app: &GPSApp) -> GraphTab {
graphtab(app, app.current_graphtab.get())
}
pub fn current_graphtab_set_filename(app: &GPSApp, filename: &str) {
app.graphbook
.borrow()
.get(&app.current_graphtab.get())
.expect("the graphtab is available")
.set_filename(filename);
}
pub fn current_graphtab_set_modified(app: &GPSApp, modified: bool) {
app.graphbook
.borrow()
.get(&app.current_graphtab.get())
.expect("the graphtab is available")
.set_modified(modified);
}
pub fn setup_graphbook(app: &GPSApp) {
let graphbook: gtk::Notebook = app
.builder
.object("graphbook")
.expect("Couldn't get the graphbook");
let app_weak = app.downgrade();
graphbook.connect_switch_page(move |_book, widget, page| {
let graphview = widget
.first_child()
.expect("Unable to get the child from the graphbook, ie the graphview");
if let Ok(graphview) = graphview.dynamic_cast::<GM::GraphView>() {
let app = upgrade_weak!(app_weak);
GPS_TRACE!("graphview.id() {} graphbook page {}", graphview.id(), page);
app.current_graphtab.set(page);
}
});
}
pub fn create_graphtab(app: &GPSApp, id: u32, name: Option<&str>) {
let graph_tab = GraphTab::new(app.downgrade(), id, name.unwrap_or("Untitled"));
let gt = graph_tab.clone();
app.graphbook.borrow_mut().insert(id, graph_tab);
let graphbook: gtk::Notebook = app
.builder
.object("graphbook")
.expect("Couldn't get graphbook");
let drawing_area_window: gtk::Viewport = gtk::Viewport::builder().build();
drawing_area_window.set_child(Some(&*graphtab(app, id).graphview()));
let tab_box = gtk::Box::new(gtk::Orientation::Horizontal, 0);
let label = gt.widget_label();
tab_box.append(label);
let icon = gtk::Image::from_icon_name("window-close-symbolic");
let close_button = gtk::Button::builder().build();
close_button.set_child(Some(&icon));
close_button.add_css_class("small-button");
close_button.add_css_class("image-button");
close_button.add_css_class("flat");
let app_weak = app.downgrade();
close_button.connect_clicked(glib::clone!(@weak graphbook => move |_| {
let app = upgrade_weak!(app_weak);
graphbook.remove_page(Some(current_graphtab(&app).id()));
}));
tab_box.append(&close_button);
graphbook.append_page(&drawing_area_window, Some(&tab_box));
graphbook.set_tab_reorderable(&drawing_area_window, true);
let app_weak = app.downgrade();
gt.graphview().connect_local(
"graph-updated",
false,
glib::clone!(@weak graphbook => @default-return None, move |values: &[Value]| {
let app = upgrade_weak!(app_weak, None);
let id = values[1].get::<u32>().expect("id in args[1]");
GPS_DEBUG!("Graph updated id={}", id);
let _ = app
.save_graph(
Settings::graph_file_path()
.to_str()
.expect("Unable to convert to string"),
)
.map_err(|e| GPS_WARN!("Unable to save file {}", e));
current_graphtab_set_modified(&app, true);
None
}),
);
let app_weak = app.downgrade();
gt.graphview().connect_local(
"node-added",
false,
glib::clone!(@weak graphbook => @default-return None, move |values: &[Value]| {
let app = upgrade_weak!(app_weak, None);
let graph_id = values[1].get::<u32>().expect("graph id in args[1]");
let node_id = values[2].get::<u32>().expect("node id in args[2]");
GPS_DEBUG!("Node added node id={} in graph id={}", node_id, graph_id);
if let Some(node) = current_graphtab(&app).graphview().node(node_id) {
let description = GPS::ElementInfo::element_description(&node.name()).ok();
node.set_tooltip_markup(description.as_deref());
if !GPS::ElementInfo::element_factory_exists(&node.name()) {
node.set_light(true);
}
for port in node.all_ports(GM::PortDirection::All) {
let caps = PropertyExt::property(&port,"_caps");
GPS_DEBUG!("caps={} for port id {}", caps.clone().unwrap_or_else(|| "caps unknown".to_string()), port.id());
port.set_tooltip_markup(caps.as_deref());
}
}
None
}),
);
let app_weak = app.downgrade();
gt.graphview().connect_local(
"port-added",
false,
glib::clone!(@weak graphbook => @default-return None, move |values: &[Value]| {
let app = upgrade_weak!(app_weak, None);
let graph_id = values[1].get::<u32>().expect("graph id in args[1]");
let node_id = values[2].get::<u32>().expect("node id in args[2]");
let port_id = values[3].get::<u32>().expect("port id in args[3]");
GPS_DEBUG!("Port added port id={} to node id={} in graph id={}", port_id, node_id, graph_id);
if let Some(node) = current_graphtab(&app).graphview().node(node_id) {
if let Some(port) = node.port(port_id) {
let caps = PropertyExt::property(&port, "_caps");
GPS_DEBUG!("caps={} for port id {}", caps.clone().unwrap_or_else(|| "caps unknown".to_string()), port.id());
port.set_tooltip_markup(caps.as_deref());
}
}
None
}),
);
// When user clicks on port with right button
let app_weak = app.downgrade();
gt.graphview()
.connect_local(
"graph-right-clicked",
false,
glib::clone!(@weak graphbook => @default-return None, move |values: &[Value]| {
let app = upgrade_weak!(app_weak, None);
let point = values[1].get::<graphene::Point>().expect("point in args[2]");
let pop_menu = app.app_pop_menu_at_position(&*current_graphtab(&app).graphview(), point.to_vec2().x() as f64, point.to_vec2().y() as f64);
let menu: gio::MenuModel = app
.builder
.object("graph_menu")
.expect("Couldn't graph_menu");
pop_menu.set_menu_model(Some(&menu));
let app_weak = app.downgrade();
app.connect_app_menu_action("graph.clear",
move |_,_| {
let app = upgrade_weak!(app_weak);
current_graphtab(&app).graphview().clear();
}
);
let app_weak = app.downgrade();
app.connect_app_menu_action("graph.check",
move |_,_| {
let app = upgrade_weak!(app_weak);
let render_parse_launch = current_graphtab(&app).player().pipeline_description_from_graphview(&current_graphtab(&app).graphview());
if current_graphtab(&app).player().create_pipeline(&render_parse_launch).is_ok() {
GPSUI::message::display_message_dialog(&render_parse_launch,gtk::MessageType::Info, |_| {});
} else {
GPSUI::message::display_error_dialog(false, &format!("Unable to render:\n\n{render_parse_launch}"));
}
}
);
let app_weak = app.downgrade();
app.connect_app_menu_action("graph.pipeline_details",
move |_,_| {
let app = upgrade_weak!(app_weak);
GPSUI::properties::display_pipeline_details(&app);
}
);
pop_menu.show();
None
}),
);
// When user clicks on port with right button
let app_weak = app.downgrade();
gt.graphview()
.connect_local("port-right-clicked", false, move |values: &[Value]| {
let app = upgrade_weak!(app_weak, None);
let port_id = values[1].get::<u32>().expect("port id args[1]");
let node_id = values[2].get::<u32>().expect("node id args[2]");
let point = values[3]
.get::<graphene::Point>()
.expect("point in args[3]");
let pop_menu = app.app_pop_menu_at_position(
&*current_graphtab(&app).graphview(),
point.to_vec2().x() as f64,
point.to_vec2().y() as f64,
);
let menu: gio::MenuModel = app
.builder
.object("port_menu")
.expect("Couldn't get menu model for port");
pop_menu.set_menu_model(Some(&menu));
if current_graphtab(&app)
.graphview()
.can_remove_port(node_id, port_id)
{
let app_weak = app.downgrade();
app.connect_app_menu_action("port.delete", move |_, _| {
let app = upgrade_weak!(app_weak);
GPS_DEBUG!("port.delete-link port id {} node id {}", port_id, node_id);
current_graphtab(&app)
.graphview()
.remove_port(node_id, port_id);
});
} else {
app.disconnect_app_menu_action("port.delete");
}
let app_weak = app.downgrade();
app.connect_app_menu_action("port.properties", move |_, _| {
let app = upgrade_weak!(app_weak);
GPS_DEBUG!("port.properties port id {} node id {}", port_id, node_id);
let node = app.node(node_id);
let port = app.port(node_id, port_id);
GPSUI::properties::display_pad_properties(
&app,
&node.name(),
&port.name(),
node_id,
port_id,
);
});
pop_menu.show();
None
});
// When user clicks on node with right button
let app_weak = app.downgrade();
gt.graphview()
.connect_local(
"node-right-clicked",
false,
glib::clone!(@weak graphbook => @default-return None, move |values: &[Value]| {
let app = upgrade_weak!(app_weak, None);
let node_id = values[1].get::<u32>().expect("node id args[1]");
let node = current_graphtab(&app).graphview().node(node_id).unwrap();
let element_exists = GPS::ElementInfo::element_factory_exists(&node.name());
let point = values[2].get::<graphene::Point>().expect("point in args[2]");
let pop_menu = app.app_pop_menu_at_position(&*current_graphtab(&app).graphview(), point.to_vec2().x() as f64, point.to_vec2().y() as f64);
let menu: gio::MenuModel = app
.builder
.object("node_menu")
.expect("Couldn't get menu model for node");
pop_menu.set_menu_model(Some(&menu));
let app_weak = app.downgrade();
app.connect_app_menu_action("node.delete",
move |_,_| {
let app = upgrade_weak!(app_weak);
GPS_DEBUG!("node.delete id: {}", node_id);
current_graphtab(&app).graphview().remove_node(node_id);
}
);
if element_exists {
let app_weak = app.downgrade();
app.connect_app_menu_action("node.add-to-favorite",
move |_,_| {
let app = upgrade_weak!(app_weak);
GPS_DEBUG!("node.add-to-favorite id: {}", node_id);
if let Some(node) = current_graphtab(&app).graphview().node(node_id) {
GPSUI::elements::add_to_favorite_list(&app, node.name());
};
}
);
let node = app.node(node_id);
if let Some(input) = GPS::ElementInfo::element_supports_new_pad_request(&node.name(), GM::PortDirection::Input) {
let app_weak = app.downgrade();
app.connect_app_menu_action("node.request-pad-input",
move |_,_| {
let app = upgrade_weak!(app_weak);
GPS_DEBUG!("node.request-pad-input id: {}", node_id);
app.create_port_with_caps(node_id, GM::PortDirection::Input, GM::PortPresence::Sometimes, input.caps().to_string());
}
);
} else {
app.disconnect_app_menu_action("node.request-pad-input");
}
let node = app.node(node_id);
if let Some(output) = GPS::ElementInfo::element_supports_new_pad_request(&node.name(), GM::PortDirection::Output) {
let app_weak = app.downgrade();
app.connect_app_menu_action("node.request-pad-output",
move |_,_| {
let app = upgrade_weak!(app_weak);
GPS_DEBUG!("node.request-pad-output id: {}", node_id);
app.create_port_with_caps(node_id, GM::PortDirection::Output, GM::PortPresence::Sometimes, output.caps().to_string());
}
);
} else {
app.disconnect_app_menu_action("node.request-pad-output");
}
let app_weak = app.downgrade();
app.connect_app_menu_action("node.properties",
move |_,_| {
let app = upgrade_weak!(app_weak);
GPS_DEBUG!("node.properties id {}", node_id);
let node = current_graphtab(&app).graphview().node(node_id).unwrap();
GPSUI::properties::display_plugin_properties(&app, &node.name(), node_id);
}
);
}
pop_menu.show();
None
}),
);
let app_weak = app.downgrade();
gt.graphview().connect_local(
"node-double-clicked",
false,
glib::clone!(@weak graphbook => @default-return None, move |values: &[Value]| {
let app = upgrade_weak!(app_weak, None);
let node_id = values[1].get::<u32>().expect("node id args[1]");
GPS_TRACE!("Node double clicked id={}", node_id);
let node = current_graphtab(&app).graphview().node(node_id).unwrap();
if GPS::ElementInfo::element_factory_exists(&node.name()) {
GPSUI::properties::display_plugin_properties(&app, &node.name(), node_id);
}
None
}),
);
let app_weak = app.downgrade();
gt.graphview().connect_local(
"link-double-clicked",
false,
glib::clone!(@weak graphbook => @default-return None, move |values: &[Value]| {
let app = upgrade_weak!(app_weak, None);
let link_id = values[1].get::<u32>().expect("link id args[1]");
GPS_TRACE!("link double clicked id={}", link_id);
let link = current_graphtab(&app).graphview().link(link_id).unwrap();
GPSUI::dialog::create_input_dialog(
"Enter caps filter description",
"description",
&link.name(),
&app,
move |app, link_desc| {
current_graphtab(&app).graphview().set_link_name(link.id(), link_desc.as_str());
GPS_DEBUG!("link double clicked id={} name={}", link.id(), link.name());
},
);
None
}),
);
}

View file

@ -144,7 +144,7 @@ mod imp {
let target = widget.pick(x, y, gtk::PickFlags::DEFAULT).expect("port pick() did not return a widget");
if let Some(target) = target.ancestor(Port::static_type()) {
let port = target.dynamic_cast::<Port>().expect("click event is not on the Port");
let node = port.ancestor(Node::static_type()).expect("Unable to reach parent").dynamic_cast::<Node>().expect("Unable to cast to Node");
let node = port.ancestor(Node::static_type()).expect("Unable to reach parent").dynamic_cast::<Node>().expect("Unable to cast to Node");
obj.emit_by_name::<()>("port-right-clicked", &[&port.id(), &node.id(), &graphene::Point::new(x as f32,y as f32)]);
} else if let Some(target) = target.ancestor(Node::static_type()) {
let node = target.dynamic_cast::<Node>().expect("click event is not on the Node");
@ -204,7 +204,6 @@ mod imp {
node_to.id(),
port_from.id(),
port_to.id(),
true,
));
}
widget.set_selected_port(None);
@ -225,7 +224,13 @@ mod imp {
info!("double clicked node id {}", node.id());
obj.emit_by_name::<()>("node-double-clicked", &[&node.id(), &graphene::Point::new(x as f32,y as f32)]);
}
} else if _n_press % 2 == 0 {
if let Some(link) = widget.point_on_link(&graphene::Point::new(x.floor() as f32,y.floor() as f32)) {
info!("double clicked link id {}", link.id());
obj.emit_by_name::<()>("link-double-clicked", &[&link.id(), &graphene::Point::new(x as f32,y as f32)]);
}
}
// Click to something else than a port
widget.set_selected_port(None);
}
@ -289,6 +294,9 @@ mod imp {
Signal::builder("port-added")
.param_types([u32::static_type(), u32::static_type(), u32::static_type()])
.build(),
Signal::builder("link-double-clicked")
.param_types([u32::static_type(), graphene::Point::static_type()])
.build(),
]
});
SIGNALS.as_ref()
@ -310,8 +318,9 @@ mod imp {
if let Some((from_x, from_y, to_x, to_y)) = self.link_coordinates(link) {
self.draw_link(
snapshot,
link.active,
link.active(),
link.selected(),
link.name().as_str(),
link.thickness as f64,
&graphene::Point::new(from_x as f32, from_y as f32),
&graphene::Point::new(to_x as f32, to_y as f32),
@ -335,6 +344,7 @@ mod imp {
snapshot,
false,
false,
"",
2.0,
&graphene::Point::new(from_x as f32, from_y as f32),
&graphene::Point::new(to_x as f32, to_y as f32),
@ -349,7 +359,7 @@ mod imp {
let from_node = nodes
.get(&node_from)
.unwrap_or_else(|| panic!("Unable to get node from {}", node_from));
.unwrap_or_else(|| (panic!("Unable to get node from {}", node_from)));
let from_port = from_node
.port(port_from)
@ -403,12 +413,13 @@ mod imp {
let (to_x, to_y) = self.link_to_coordinates(link.node_to, link.port_to);
Some((from_x, from_y, to_x, to_y))
}
#[allow(clippy::too_many_arguments)]
fn draw_link(
&self,
snapshot: &gtk::Snapshot,
active: bool,
selected: bool,
name: &str,
thickness: f64,
point_from: &graphene::Point,
point_to: &graphene::Point,
@ -441,6 +452,13 @@ mod imp {
if let Err(e) = link_cr.stroke() {
warn!("Failed to draw graphview links: {}", e);
};
trace!("the link name is {}", name);
if !name.is_empty() {
let x = (point_from.x() + point_to.x()) / 2.0 + 20.0;
let y = (point_from.y() + point_to.y()) / 2.0 + 20.0;
link_cr.move_to(x as f64, y as f64);
let _ = link_cr.show_text(name);
}
}
}
}
@ -459,7 +477,7 @@ impl GraphView {
// Load CSS from the STYLE variable.
let provider = gtk::CssProvider::new();
provider.load_from_data(GRAPHVIEW_STYLE);
gtk::StyleContext::add_provider_for_display(
gtk::style_context_add_provider_for_display(
&gtk::gdk::Display::default().expect("Error initializing gtk css provider."),
&provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
@ -474,7 +492,7 @@ impl GraphView {
private.id.set(id)
}
/// Retrives the graphview id
/// Retrieves the graphview id
///
pub fn id(&self) -> u32 {
let private = imp::GraphView::from_obj(self);
@ -526,7 +544,7 @@ impl GraphView {
let private = imp::GraphView::from_obj(self);
node.set_parent(self);
// Place widgets in colums of 3, growing down
// Place widgets in columns of 3, growing down
let x = if let Some(node_type) = node.node_type() {
match node_type {
NodeType::Source => 20.0,
@ -575,7 +593,7 @@ impl GraphView {
}
node.unparent();
} else {
warn!("Tried to remove non-existant node (id={}) from graph", id);
warn!("Tried to remove non-existent node (id={}) from graph", id);
}
self.queue_draw();
}
@ -739,7 +757,6 @@ impl GraphView {
node_to_id: u32,
port_from_id: u32,
port_to_id: u32,
active: bool,
) -> Link {
self.create_link_with_id(
self.next_link_id(),
@ -747,7 +764,6 @@ impl GraphView {
node_to_id,
port_from_id,
port_to_id,
active,
)
}
@ -766,7 +782,7 @@ impl GraphView {
pub fn set_link_state(&self, link_id: u32, active: bool) {
let private = imp::GraphView::from_obj(self);
if let Some(link) = private.links.borrow_mut().get_mut(&link_id) {
link.active = active;
link.set_active(active);
self.queue_draw();
} else {
warn!("Link state changed on unknown link (id={})", link_id);
@ -781,12 +797,37 @@ impl GraphView {
let links = private.links.borrow();
let links_list: Vec<_> = links
.iter()
.filter(|(_, link)| link.active == link_state)
.filter(|(_, link)| link.active() == link_state)
.map(|(_, node)| node.clone())
.collect();
links_list
}
/// Get the link with the specified link id inside the graphview.
///
/// Returns `None` if the link is not in the graphview.
pub fn link(&self, id: u32) -> Option<Link> {
let private = imp::GraphView::from_obj(self);
private.links.borrow().get(&id).cloned()
}
/// Set the link state with ink id and link state (boolean)
///
pub fn set_link_name(&self, link_id: u32, name: &str) {
let private = imp::GraphView::from_obj(self);
let mut updated = false;
if let Some(link) = private.links.borrow_mut().get_mut(&link_id) {
link.set_name(name);
self.queue_draw();
updated = true;
} else {
warn!("Link name changed on unknown link (id={})", link_id);
}
if updated {
self.graph_updated();
}
}
/// Retrieves the node/port id connected to the input port id
///
pub fn port_connected_to(&self, port_id: u32) -> Option<(u32, u32)> {
@ -799,6 +840,18 @@ impl GraphView {
None
}
/// Retrieves the link connected to the port id
///
pub fn port_link(&self, port_id: u32) -> Option<Link> {
let private = imp::GraphView::from_obj(self);
for (_id, link) in private.links.borrow().iter() {
if port_id == link.port_from {
return Some(link.clone());
}
}
None
}
/// Delete the selected element (link, node, port)
///
pub fn delete_selected(&self) {
@ -891,7 +944,8 @@ impl GraphView {
.attr("node_to", &link.node_to.to_string())
.attr("port_from", &link.port_from.to_string())
.attr("port_to", &link.port_to.to_string())
.attr("active", &link.active.to_string()),
.attr("name", &link.name())
.attr("active", &link.active().to_string()),
)?;
writer.write(XMLWEvent::end_element())?;
}
@ -1022,14 +1076,21 @@ impl GraphView {
let active: &String = attrs
.get::<String>(&String::from("active"))
.expect("Unable to find link state");
current_link = Some(self.create_link_with_id(
let default_value = String::from("");
let name: &String = attrs
.get::<String>(&String::from("name"))
.unwrap_or(&default_value);
let link = self.create_link_with_id(
id.parse::<u32>().unwrap(),
node_from.parse::<u32>().unwrap(),
node_to.parse::<u32>().unwrap(),
port_from.parse::<u32>().unwrap(),
port_to.parse::<u32>().unwrap(),
active.parse::<bool>().unwrap(),
));
);
link.set_active(active.parse::<bool>().unwrap());
link.set_name(name.parse::<String>().unwrap().as_str());
current_link = Some(link);
}
_ => warn!("name unknown: {}", name),
}
@ -1113,17 +1174,8 @@ impl GraphView {
node_to_id: u32,
port_from_id: u32,
port_to_id: u32,
active: bool,
) -> Link {
Link::new(
link_id,
node_from_id,
node_to_id,
port_from_id,
port_to_id,
active,
false,
)
Link::new(link_id, node_from_id, node_to_id, port_from_id, port_to_id)
}
fn remove_link(&self, id: u32) {

View file

@ -8,7 +8,7 @@
// SPDX-License-Identifier: GPL-3.0-only
use super::SelectionExt;
use std::cell::Cell;
use std::cell::{Cell, RefCell};
#[derive(Debug, Clone)]
pub struct Link {
@ -17,44 +17,49 @@ pub struct Link {
pub node_to: u32,
pub port_from: u32,
pub port_to: u32,
pub active: bool,
pub active: Cell<bool>,
pub selected: Cell<bool>,
pub thickness: u32,
pub name: RefCell<String>,
}
impl Link {
pub fn name(&self) -> String {
self.name.borrow().clone()
}
pub fn set_name(&self, name: &str) {
self.name.replace(name.to_string());
}
pub fn id(&self) -> u32 {
self.id
}
pub fn active(&self) -> bool {
self.active.get()
}
pub fn set_active(&self, active: bool) {
self.active.set(active)
}
}
pub trait LinkExt {
/// Create a new link
///
fn new(
id: u32,
node_from: u32,
node_to: u32,
port_from: u32,
port_to: u32,
active: bool,
selected: bool,
) -> Self;
fn new(id: u32, node_from: u32, node_to: u32, port_from: u32, port_to: u32) -> Self;
}
impl LinkExt for Link {
fn new(
id: u32,
node_from: u32,
node_to: u32,
port_from: u32,
port_to: u32,
active: bool,
selected: bool,
) -> Self {
fn new(id: u32, node_from: u32, node_to: u32, port_from: u32, port_to: u32) -> Self {
Self {
id,
node_from,
node_to,
port_from,
port_to,
active,
selected: Cell::new(selected),
active: Cell::new(true),
selected: Cell::new(false),
thickness: 4,
name: RefCell::new("".to_string()),
}
}
}

View file

@ -6,7 +6,6 @@ mod property;
mod selection;
pub use graphview::GraphView;
pub use link::Link;
pub use node::Node;
pub use node::NodeType;
pub use port::Port;
@ -104,11 +103,15 @@ mod test {
// Ports have been created by create_node_with_port
//Create link between node1 and node 2
let link = graphview.create_link(1, 2, 1, 2, true);
let link = graphview.create_link(1, 2, 1, 2);
assert_eq!(&link.name(), "");
assert!(&link.active());
link.set_name("link1");
assert_eq!(&link.name(), "link1");
graphview.add_link(link);
//Create link between node2 and node 3
let link = graphview.create_link(2, 3, 3, 4, true);
let link = graphview.create_link(2, 3, 3, 4);
graphview.add_link(link);
// Save the graphview in XML into a buffer

View file

@ -48,7 +48,7 @@ impl PortDirection {
/// Port's presence
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Copy)]
pub enum PortPresence {
/// Can not be removed from his parent independantly
/// Can not be removed from his parent independently
Always,
/// Can be removed from a node
Sometimes,

View file

@ -6,7 +6,6 @@
//
// SPDX-License-Identifier: GPL-3.0-only
use gtk::glib::Sender;
use log::{debug, error, info, trace, warn};
use simplelog::*;
use std::fmt;
@ -14,6 +13,13 @@ use std::io;
use std::fs::File;
use chrono::Local;
use std::sync::Mutex;
lazy_static::lazy_static! {
static ref MSG_LOGGER: Mutex<Option<MessageLogger>> = Mutex::new(None);
}
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum LogLevel {
@ -25,6 +31,13 @@ pub enum LogLevel {
Trace,
}
#[derive(Debug, Clone, PartialEq)]
pub enum LogType {
App,
Gst,
Message,
}
impl LogLevel {
pub fn from_u32(value: u32) -> LogLevel {
match value {
@ -49,7 +62,7 @@ impl fmt::Display for LogLevel {
macro_rules! GPS_ERROR (
() => ($crate::print!("\n"));
($($arg:tt)*) => ({
logger::print_log(logger::LogLevel::Error, format_args!($($arg)*).to_string());
logger::print_log(logger::LogLevel::Error, format!("{}\t{}",glib::function_name!(),format_args!($($arg)*).to_string()));
})
);
@ -57,7 +70,7 @@ macro_rules! GPS_ERROR (
macro_rules! GPS_WARN (
() => ($crate::print!("\n"));
($($arg:tt)*) => ({
logger::print_log(logger::LogLevel::Warning, format_args!($($arg)*).to_string());
logger::print_log(logger::LogLevel::Warning, format!("{}\t{}",glib::function_name!(),format_args!($($arg)*).to_string()));
})
);
@ -65,7 +78,7 @@ macro_rules! GPS_WARN (
macro_rules! GPS_INFO (
() => ($crate::print!("\n"));
($($arg:tt)*) => ({
logger::print_log(logger::LogLevel::Info, format_args!($($arg)*).to_string());
logger::print_log(logger::LogLevel::Info, format!("{}\t{}",glib::function_name!(),format_args!($($arg)*).to_string()));
})
);
@ -73,7 +86,23 @@ macro_rules! GPS_INFO (
macro_rules! GPS_DEBUG (
() => ($crate::print!("\n"));
($($arg:tt)*) => ({
logger::print_log(logger::LogLevel::Debug, format_args!($($arg)*).to_string());
logger::print_log(logger::LogLevel::Debug, format!("{}\t{}",glib::function_name!(),format_args!($($arg)*).to_string()));
})
);
#[macro_export]
macro_rules! GPS_MSG_LOG (
() => ($crate::print!("\n"));
($($arg:tt)*) => ({
logger::print_msg_logger(logger::LogType::Message, format_args!($($arg)*).to_string());
})
);
#[macro_export]
macro_rules! GPS_GST_LOG (
() => ($crate::print!("\n"));
($($arg:tt)*) => ({
logger::print_msg_logger(logger::LogType::Gst, format_args!($($arg)*).to_string());
})
);
@ -86,7 +115,7 @@ macro_rules! GPS_TRACE (
);
struct WriteAdapter {
sender: Sender<String>,
sender: async_channel::Sender<(LogType, String)>,
buffer: String,
}
@ -97,8 +126,8 @@ impl io::Write for WriteAdapter {
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
if self.buffer.ends_with('\n') {
self.buffer.pop();
self.sender.send(self.buffer.clone()).unwrap();
self.buffer = String::from("");
let _ = self.sender.try_send((LogType::App, self.buffer.clone()));
self.buffer.clear();
}
Ok(buf.len())
@ -120,7 +149,7 @@ fn translate_to_simple_logger(log_level: LogLevel) -> LevelFilter {
}
}
pub fn init_logger(sender: Sender<String>, log_file: &str) {
pub fn init_logger(sender: async_channel::Sender<(LogType, String)>, log_file: &str) {
simplelog::CombinedLogger::init(vec![
WriteLogger::new(
translate_to_simple_logger(LogLevel::Trace),
@ -169,3 +198,36 @@ pub fn print_log(log_level: LogLevel, msg: String) {
_ => {}
};
}
#[derive(Debug, Clone)]
pub struct MessageLogger {
sender: async_channel::Sender<(LogType, String)>,
}
impl MessageLogger {
pub fn new(sender: async_channel::Sender<(LogType, String)>) -> Self {
Self { sender }
}
pub fn print_log(&self, log_type: LogType, msg: String) {
let to_send = format!("{}\t{}", Local::now().format("%H:%M:%S"), msg);
self.sender
.try_send((log_type, to_send))
.expect("Unable to send the log");
}
}
pub fn init_msg_logger(sender: async_channel::Sender<(LogType, String)>) {
let mut msg_logger = MSG_LOGGER.lock().unwrap();
if msg_logger.is_none() {
// Initialize the variable
*msg_logger = Some(MessageLogger::new(sender));
}
}
pub fn print_msg_logger(log_type: LogType, msg: String) {
let msg_logger = MSG_LOGGER.lock().unwrap();
if let Some(logger) = msg_logger.as_ref() {
logger.print_log(log_type, msg);
}
}

View file

@ -11,6 +11,7 @@ mod macros;
mod app;
mod common;
mod config;
mod graphbook;
mod graphmanager;
mod ui;
#[macro_use]
@ -21,14 +22,29 @@ use gtk::prelude::*;
use crate::app::GPSApp;
use crate::common::init;
use structopt::StructOpt;
#[derive(StructOpt, Debug)]
struct Command {
#[structopt(about = "Sets the pipeline description", default_value = "")]
pipeline: String,
}
fn main() {
// gio::resources_register_include!("compiled.gresource").unwrap();
init().expect("Unable to init app");
let application = gtk::Application::new(Some(config::APP_ID), Default::default());
let application = gtk::Application::new(
Some(config::APP_ID),
gtk::gio::ApplicationFlags::HANDLES_COMMAND_LINE,
);
application.connect_startup(|application| {
GPSApp::on_startup(application);
let args = Command::from_args();
GPSApp::on_startup(application, &args.pipeline);
});
application.connect_command_line(|_app, _cmd_line| {
// structopt already handled arguments
0
});
application.run();
}

View file

@ -1,6 +1,5 @@
conf = configuration_data()
conf.set_quoted('APP_ID', application_id)
conf.set_quoted('VERSION', version + version_suffix)
configure_file(
input: 'config.rs.in',
@ -9,7 +8,7 @@ configure_file(
)
# Copy the config.rs output to the source directory.
run_command(python3, '-c', 'import shutil; shutil.copyfile("@0@", "@1@")'.format(join_paths(meson.build_root(), 'src', 'config.rs'), join_paths(meson.source_root(), 'src', 'config.rs')))
run_command(python3, '-c', 'import shutil; shutil.copyfile("@0@", "@1@")'.format(join_paths(meson.project_build_root(), 'src', 'config.rs'), join_paths(meson.project_source_root(), 'src', 'config.rs')))
rust_sources = files(
'gps/player.rs',
@ -33,13 +32,12 @@ rust_sources = files(
'logger.rs',
'macros.rs',
'main.rs',
'settings.rs',
)
sources = [cargo_sources, rust_sources]
cargo_script = find_program(join_paths(meson.source_root(), 'build-aux/cargo.py'))
cargo_script = find_program(join_paths(meson.project_source_root(), 'build-aux/cargo.py'))
app_name = meson.project_name()
if host_system == 'windows'
@ -56,8 +54,8 @@ cargo_release = custom_target(
install_dir: get_option('bindir'),
command: [
cargo_script,
meson.build_root(),
meson.source_root(),
meson.project_build_root(),
meson.project_source_root(),
'@OUTPUT@',
get_option('buildtype'),
app_name,

View file

@ -31,45 +31,55 @@ pub struct Settings {
}
impl Settings {
fn settings_file_exist() {
let s = Settings::settings_file_path();
fn create_path_if_not(s: &PathBuf) {
if !s.exists() {
if let Some(parent_dir) = s.parent() {
if !parent_dir.exists() {
if let Err(e) = create_dir_all(parent_dir) {
GPS_ERROR!(
"Error while trying to build settings snapshot_directory '{}': {}",
parent_dir.display(),
e
);
}
}
if let Err(e) = create_dir_all(s) {
GPS_ERROR!(
"Error while trying to build settings snapshot_directory '{}': {}",
s.display(),
e
);
}
}
}
fn settings_file_path() -> PathBuf {
fn default_app_folder() -> PathBuf {
let mut path = glib::user_config_dir();
path.push(config::APP_ID);
path
}
fn settings_file_path() -> PathBuf {
let mut path = Settings::default_app_folder();
Settings::create_path_if_not(&path);
path.push("settings.toml");
path
}
// Public methods
pub fn default_graph_file_path() -> PathBuf {
let mut path = glib::user_config_dir();
path.push(config::APP_ID);
pub fn graph_file_path() -> PathBuf {
let mut path = Settings::default_app_folder();
Settings::create_path_if_not(&path);
path.push("default_graph.toml");
path
}
pub fn default_log_file_path() -> PathBuf {
let mut path = glib::user_config_dir();
path.push(config::APP_ID);
pub fn log_file_path() -> PathBuf {
let mut path = Settings::default_app_folder();
Settings::create_path_if_not(&path);
path.push("gstpipelinestudio.log");
path
}
pub fn gst_log_level() -> String {
let settings = Settings::load_settings();
let binding = "0".to_string();
let level = settings
.preferences
.get("gst_log_level")
.unwrap_or(&binding);
level.clone()
}
pub fn set_recent_pipeline_description(pipeline: &str) {
let mut settings = Settings::load_settings();
settings.recent_pipeline = pipeline.to_string();
@ -105,7 +115,6 @@ impl Settings {
// Save the provided settings to the settings path
pub fn save_settings(settings: &Settings) {
Settings::settings_file_exist();
let s = Settings::settings_file_path();
if let Err(e) = serde_any::to_file(&s, settings) {
GPS_ERROR!("Error while trying to save file: {} {}", s.display(), e);

View file

@ -11,17 +11,21 @@
<item>
<attribute name="label" translatable="yes" comments="Primary menu entry that opens the graph">_Open</attribute>
<attribute name="action">app.open</attribute>
<attribute name="accel">&lt;primary&gt;n</attribute>
<attribute name="accel">&lt;primary&gt;o</attribute>
</item>
<item>
<attribute name="label" translatable="yes" comments="Primary menu entry that opens a pipeline">_Open pipeline</attribute>
<attribute name="action">app.open_pipeline</attribute>
<attribute name="accel">&lt;primary&gt;p</attribute>
</item>
<item>
<attribute name="label" translatable="yes" comments="Primary menu entry that saves the graph">_Save</attribute>
<attribute name="action">app.save</attribute>
<attribute name="accel">&lt;primary&gt;s</attribute>
</item>
<item>
<attribute name="label" translatable="yes" comments="Primary menu entry that saves the graph">_Save As</attribute>
<attribute name="action">app.save_as</attribute>
<attribute name="accel">&lt;primary&gt;n</attribute>
</item>
<item>
<attribute name="label" translatable="yes" comments="Primary menu entry that open the preferences">_Preferences</attribute>
@ -48,6 +52,10 @@
</menu>
<menu id="graph_menu">
<section>
<item>
<attribute name="label" translatable="yes" comments="graph menu entry clear graph">_Clear graph</attribute>
<attribute name="action">app.graph.clear</attribute>
</item>
<item>
<attribute name="label" translatable="yes" comments="graph menu entry check graph">_Check graph</attribute>
<attribute name="action">app.graph.check</attribute>
@ -205,7 +213,7 @@
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="child">
<object class="GtkViewport" id="drawing_area">
<object class="GtkNotebook" id="graphbook">
<child>
<placeholder/>
</child>
@ -214,11 +222,56 @@
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="child">
<object class="GtkTreeView" id="treeview-logger"/>
</property>
</object>
<object class="GtkNotebook" id="notebook-debug">
<child>
<object class="GtkNotebookPage">
<property name="child">
<object class="GtkScrolledWindow">
<property name="child">
<object class="GtkTreeView" id="treeview-app-logger"/>
</property>
</object>
</property>
<property name="tab">
<object class="GtkLabel" id="label-app-logger">
<property name="label" translatable="1">App</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkNotebookPage">
<property name="child">
<object class="GtkScrolledWindow">
<property name="child">
<object class="GtkTreeView" id="treeview-gst-logger"/>
</property>
</object>
</property>
<property name="tab">
<object class="GtkLabel" id="label-gst-logger">
<property name="label" translatable="1">GST</property>
</object>
</property>
</object>
</child>
<child>
<object class="GtkNotebookPage">
<property name="child">
<object class="GtkScrolledWindow">
<property name="child">
<object class="GtkTreeView" id="treeview-msg-logger"/>
</property>
</object>
</property>
<property name="tab">
<object class="GtkLabel" id="label-messages-logger">
<property name="label" translatable="1">Messages</property>
</object>
</property>
</object>
</child>
</object>
</child>
</object>
</child>

View file

@ -7,6 +7,7 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::app::GPSApp;
use crate::logger;
use crate::ui::treeview;
use gtk::prelude::*;
use gtk::{gio, glib};
@ -18,18 +19,37 @@ fn reset_logger_list(logger_list: &TreeView) {
String::static_type(),
String::static_type(),
String::static_type(),
String::static_type(),
String::static_type(),
]);
logger_list.set_model(Some(&model));
}
pub fn setup_logger_list(app: &GPSApp) {
treeview::add_column_to_treeview(app, "treeview-logger", "TIME", 0, false);
treeview::add_column_to_treeview(app, "treeview-logger", "LEVEL", 1, false);
treeview::add_column_to_treeview(app, "treeview-logger", "LOG", 2, true);
pub fn setup_logger_list(app: &GPSApp, logger_name: &str, log_type: logger::LogType) {
match log_type {
logger::LogType::App => {
treeview::add_column_to_treeview(app, logger_name, "TIME", 0, false);
treeview::add_column_to_treeview(app, logger_name, "LEVEL", 1, false);
treeview::add_column_to_treeview(app, logger_name, "LOG", 2, true);
}
logger::LogType::Gst => {
treeview::add_column_to_treeview(app, logger_name, "TIME", 0, false);
treeview::add_column_to_treeview(app, logger_name, "LEVEL", 1, false);
treeview::add_column_to_treeview(app, logger_name, "CATEGORY", 2, false);
treeview::add_column_to_treeview(app, logger_name, "FILE", 3, false);
treeview::add_column_to_treeview(app, logger_name, "LOG", 4, true);
}
logger::LogType::Message => {
treeview::add_column_to_treeview(app, logger_name, "TIME", 0, false);
treeview::add_column_to_treeview(app, logger_name, "LEVEL", 1, false);
treeview::add_column_to_treeview(app, logger_name, "LOG", 2, true);
}
}
let logger_list: TreeView = app
.builder
.object("treeview-logger")
.expect("Couldn't get treeview-logger");
.object(logger_name)
.expect("Couldn't get treeview-app-logger");
reset_logger_list(&logger_list);
let gesture = gtk::GestureClick::new();
@ -59,17 +79,45 @@ pub fn setup_logger_list(app: &GPSApp) {
logger_list.add_controller(gesture);
}
pub fn add_to_logger_list(app: &GPSApp, log_entry: &str) {
fn log_tree_id_from_log_type(log_type: logger::LogType) -> String {
match log_type {
logger::LogType::App => String::from("treeview-app-logger"),
logger::LogType::Gst => String::from("treeview-gst-logger"),
logger::LogType::Message => String::from("treeview-msg-logger"),
}
}
pub fn add_to_logger_list(app: &GPSApp, log_type: logger::LogType, log_entry: &str) {
let log_tree_name = log_tree_id_from_log_type(log_type.clone());
let logger_list: TreeView = app
.builder
.object("treeview-logger")
.expect("Couldn't get treeview-logger");
.object(log_tree_name.as_str())
.expect("Couldn't get treeview");
if let Some(model) = logger_list.model() {
let list_store = model
.dynamic_cast::<ListStore>()
.expect("Could not cast to ListStore");
let log: Vec<&str> = log_entry.splitn(3, ' ').collect();
list_store.insert_with_values(Some(0), &[(0, &log[0]), (1, &log[1]), (2, &log[2])]);
if log_type == logger::LogType::Gst {
let log: Vec<&str> = log_entry.splitn(5, '\t').collect();
list_store.insert_with_values(
Some(0),
&[
(0, &log[0]),
(1, &log[1]),
(2, &log[2]),
(3, &log[3]),
(4, &log[4]),
],
);
} else {
let log: Vec<&str> = log_entry.splitn(3, ' ').collect();
let mut indexed_vec: Vec<(u32, &dyn ToValue)> = Vec::new();
for (index, item) in log.iter().enumerate() {
indexed_vec.push((index as u32, item));
}
list_store.insert_with_values(Some(0), &indexed_vec);
}
// Scroll to the first element.
if let Some(model) = logger_list.model() {
if let Some(iter) = model.iter_first() {

View file

@ -82,5 +82,16 @@ pub fn display_settings(app: &GPSApp) {
dialog.close();
});
let widget = gtk::Entry::new();
widget.set_text(settings::Settings::gst_log_level().as_str());
widget.connect_changed(glib::clone!(@weak widget => move |c| {
let mut settings = settings::Settings::load_settings();
settings.preferences.insert("gst_log_level".to_string(), c.text().to_string());
settings::Settings::save_settings(&settings);
}));
let widget = widget
.dynamic_cast::<gtk::Widget>()
.expect("Should be a widget");
add_settings_widget(&grid, "GST Log level", &widget, 2);
dialog.show();
}

View file

@ -9,6 +9,7 @@
use crate::app::GPSApp;
use crate::common;
use crate::gps as GPS;
use crate::graphbook;
use crate::logger;
use crate::ui as GPSUI;
use crate::{GPS_INFO, GPS_TRACE};
@ -56,6 +57,7 @@ pub fn property_to_widget<F: Fn(String, String) + 'static>(
glib::ParamSpecInt64::static_type(),
glib::ParamSpecUInt64::static_type(),
glib::ParamSpecString::static_type(),
glib::ParamSpecFloat::static_type(),
]
.contains(&t) =>
{
@ -112,7 +114,7 @@ pub fn property_to_widget<F: Fn(String, String) + 'static>(
let param = param
.clone()
.downcast::<glib::ParamSpecFlags>()
.expect("Should be a ParamSpecEnum");
.expect("Should be a ParamSpecFlags");
let flags = param.flags_class();
for value in flags.values() {
combo.append_text(&format!(
@ -300,7 +302,10 @@ pub fn display_pipeline_details(app: &GPSApp) {
grid.set_row_spacing(4);
grid.set_margin_bottom(12);
if let Some(elements) = app.player.borrow().pipeline_elements() {
if let Some(elements) = graphbook::current_graphtab(app)
.player()
.pipeline_elements()
{
let elements_list = elements.join(" ");
let label = gtk::Label::builder()
.label(format!("{} elements:", elements.len()))

View file

@ -0,0 +1,8 @@
[wrap-git]
directory=gstreamer-1.0
url=https://gitlab.freedesktop.org/gstreamer/gstreamer.git
push-url=git@gitlab.freedesktop.org:gstreamer/gstreamer.git
revision=1.22
[provide]
dependency_names = gstreamer-1.0, gstreamer-base-1.0, gstreamer-sys-1.0, gstreamer-plugins-bad-1.0, gstreamer-video-1.0, gstreamer-player-1.0

9
subprojects/gtk.wrap Normal file
View file

@ -0,0 +1,9 @@
[wrap-git]
directory=gtk
url=https://gitlab.gnome.org/GNOME/gtk.git
push-url=ssh://git@gitlab.gnome.org:GNOME/gtk.git
revision=4.8.2
depth=1
[provide]
dependency_names=gtk4