Compare commits
11 commits
bc9d118de0
...
22e5890a7c
| Author | SHA1 | Date | |
|---|---|---|---|
| 22e5890a7c | |||
|
|
d1f63c570c | ||
|
|
dbf4b87661 | ||
|
|
0a0f44773f | ||
|
|
fab830c994 | ||
|
|
f3d6469bed | ||
|
|
468b5f1459 | ||
|
|
2aa2c841fa | ||
|
|
a8d9416754 | ||
|
|
55357a896a | ||
|
|
b44c8d2745 |
64 changed files with 3934 additions and 1465 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -5,4 +5,8 @@
|
|||
/debian/files
|
||||
/target/
|
||||
/vendor/
|
||||
!/vendor/
|
||||
/vendor/*
|
||||
!/vendor/iced_video_player/
|
||||
!/vendor/iced_video_player/**
|
||||
/vendor.tar
|
||||
|
|
|
|||
15
.zed/settings.json
Normal file
15
.zed/settings.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"format_on_save": "on",
|
||||
"lsp": {
|
||||
"rust-analyzer": {
|
||||
"initialization_options": {
|
||||
"check": {
|
||||
"command": "clippy",
|
||||
},
|
||||
"rustfmt": {
|
||||
"extraArgs": ["+nightly"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
2057
Cargo.lock
generated
2057
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
43
Cargo.toml
43
Cargo.toml
|
|
@ -1,56 +1,59 @@
|
|||
[package]
|
||||
name = "cosmic-player"
|
||||
version = "1.0.9"
|
||||
version = "1.0.13"
|
||||
edition = "2024"
|
||||
rust-version = "1.93"
|
||||
|
||||
[build-dependencies]
|
||||
vergen = { version = "8", features = ["git", "gitcl"] }
|
||||
xdgen = "0.1"
|
||||
|
||||
[dependencies]
|
||||
ashpd = { version = "0.12", optional = true }
|
||||
gstreamer-tag = "0.23"
|
||||
image = "0.24.9"
|
||||
gstreamer-tag = "0.25"
|
||||
image = "0.25"
|
||||
serde = { version = "1", features = ["serde_derive"] }
|
||||
tempfile = "3"
|
||||
tokio = { version = "1", features = ["sync"] }
|
||||
url = { version = "2", features = ["serde"] }
|
||||
# CLI arguments
|
||||
clap_lex = "0.7"
|
||||
clap_lex = "1.1"
|
||||
# Internationalization
|
||||
icu_collator = "1.5"
|
||||
icu_provider = { version = "1.5", features = ["sync"] }
|
||||
i18n-embed = { version = "0.14", features = [
|
||||
icu_collator = "2.2"
|
||||
icu_locale = "2.2"
|
||||
i18n-embed = { version = "0.16", features = [
|
||||
"fluent-system",
|
||||
"desktop-requester",
|
||||
] }
|
||||
i18n-embed-fl = "0.7"
|
||||
i18n-embed-fl = "0.10"
|
||||
rust-embed = "8"
|
||||
# Logging
|
||||
env_logger = "0.10"
|
||||
env_logger = "0.11"
|
||||
log = "0.4"
|
||||
|
||||
[dependencies.iced_video_player]
|
||||
git = "https://github.com/wash2/iced_video_player.git"
|
||||
branch = "iced-rebase"
|
||||
path = "vendor/iced_video_player"
|
||||
default-features = false
|
||||
|
||||
[dependencies.libcosmic]
|
||||
git = "https://github.com/pop-os/libcosmic.git"
|
||||
[dependencies.cosmic]
|
||||
package = "libcosmic-yoda"
|
||||
path = "../libcosmic"
|
||||
default-features = false
|
||||
features = ["advanced-shaping", "tokio", "winit", "multi-window"]
|
||||
|
||||
[dependencies.mpris-server]
|
||||
version = "0.8.1"
|
||||
version = "0.10"
|
||||
features = ["tokio"]
|
||||
optional = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
fork = "0.2"
|
||||
fork = "0.7"
|
||||
|
||||
[features]
|
||||
default = ["mpris-server", "xdg-portal", "wgpu", "wayland"]
|
||||
xdg-portal = ["ashpd", "libcosmic/xdg-portal"]
|
||||
wgpu = ["iced_video_player/wgpu", "libcosmic/wgpu"]
|
||||
wayland = ["libcosmic/wayland"]
|
||||
xdg-portal = ["ashpd", "cosmic/xdg-portal"]
|
||||
wgpu = ["iced_video_player/wgpu", "cosmic/wgpu"]
|
||||
wayland = ["cosmic/wayland"]
|
||||
|
||||
[profile.release-with-debug]
|
||||
inherits = "release"
|
||||
|
|
@ -59,10 +62,6 @@ debug = true
|
|||
# [patch.'https://github.com/wash2/iced_video_player']
|
||||
# iced_video_player = { path = "../iced_video_player" }
|
||||
|
||||
# [patch.'https://github.com/pop-os/libcosmic']
|
||||
# libcosmic = { path = "../libcosmic" }
|
||||
# cosmic-config = { path = "../libcosmic/cosmic-config" }
|
||||
# cosmic-theme = { path = "../libcosmic/cosmic-theme" }
|
||||
# libcosmic = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
|
||||
# cosmic-config = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
|
||||
# cosmic-theme = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
|
||||
|
|
|
|||
32
README.md
32
README.md
|
|
@ -1,2 +1,34 @@
|
|||
# cosmic-player
|
||||
WIP COSMIC media player
|
||||
|
||||
## Yoda local fork notes
|
||||
|
||||
This fork is used for the local COSMIC desktop build.
|
||||
|
||||
Local integration changes:
|
||||
|
||||
- `cosmic-player` depends on the local `libcosmic-yoda` crate from `../libcosmic`
|
||||
instead of upstream `pop-os/libcosmic`.
|
||||
- `iced_video_player` is vendored in `vendor/iced_video_player` so it uses the
|
||||
same local `libcosmic-yoda` and `iced_wgpu` stack. This avoids mixing upstream
|
||||
and local Iced/libcosmic types.
|
||||
- The app now runs `App::close()` before application exit and before the Quit
|
||||
menu action exits the runtime. This lets the GStreamer pipeline be dropped
|
||||
normally instead of short-circuiting through `process::exit(0)`.
|
||||
- Thumbnail generation has a bounded wait for video frames. Some files/codecs can
|
||||
fail to produce a frame; the thumbnailer now returns an error instead of
|
||||
spinning forever as a background `cosmic-player --thumbnail ...` process.
|
||||
|
||||
Validation commands:
|
||||
|
||||
```bash
|
||||
cargo +1.93.0 check
|
||||
cargo +1.93.0 tree -i libcosmic-yoda --depth 2
|
||||
cargo +1.93.0 tree -i libcosmic --depth 2 || true
|
||||
```
|
||||
|
||||
Local redeploy:
|
||||
|
||||
```bash
|
||||
COSMIC_PLAYER_REPO=/home/lionel/Projets/COSMIC/cosmic-player ./redeploy.sh
|
||||
```
|
||||
|
|
|
|||
22
build.rs
22
build.rs
|
|
@ -1,5 +1,27 @@
|
|||
use vergen::EmitBuilder;
|
||||
use std::{env, fs, path::PathBuf};
|
||||
use xdgen::{App, Context, FluentString};
|
||||
|
||||
fn main() {
|
||||
EmitBuilder::builder().git_sha(true).emit().unwrap();
|
||||
|
||||
let id = "com.system76.CosmicPlayer";
|
||||
let ctx = Context::new("i18n", env::var("CARGO_PKG_NAME").unwrap()).unwrap();
|
||||
let app = App::new(FluentString("xdg-name"))
|
||||
.comment(FluentString("xdg-comment"))
|
||||
.keywords(FluentString("xdg-keywords"));
|
||||
let output = PathBuf::from("target/xdgen");
|
||||
fs::create_dir_all(&output).unwrap();
|
||||
fs::write(
|
||||
output.join(format!("{}.desktop", id)),
|
||||
app.expand_desktop(format!("res/{}.desktop", id), &ctx)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
output.join(format!("{}.metainfo.xml", id)),
|
||||
app.expand_metainfo(format!("res/{}.metainfo.xml", id), &ctx)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
|
|
|||
18
debian/changelog
vendored
18
debian/changelog
vendored
|
|
@ -1,3 +1,21 @@
|
|||
cosmic-player (1.0.13) noble; urgency=medium
|
||||
|
||||
* Epoch 1.0.13 version update
|
||||
|
||||
-- Jeremy Soller <jeremy@system76.com> Tue, 12 May 2026 09:39:28 -0600
|
||||
|
||||
cosmic-player (1.0.12) noble; urgency=medium
|
||||
|
||||
* Epoch 1.0.12 version update
|
||||
|
||||
-- Jeremy Soller <jeremy@system76.com> Tue, 05 May 2026 10:24:29 -0600
|
||||
|
||||
cosmic-player (1.0.11) noble; urgency=medium
|
||||
|
||||
* Epoch 1.0.11 version update
|
||||
|
||||
-- Jeremy Soller <jeremy@system76.com> Tue, 14 Apr 2026 11:09:58 -0600
|
||||
|
||||
cosmic-player (1.0.9) noble; urgency=medium
|
||||
|
||||
* Epoch 1.0.9 version update
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
album = ألبوم: { $album }
|
||||
audio = الصوت
|
||||
no-video-or-audio-file-open = لا يوجد ملف فيديو أو صوت مفتوح
|
||||
open-file = افتح ملفًا
|
||||
open-folder = افتح مجلد
|
||||
subtitles = الترجمة
|
||||
open-file = افتح ملف
|
||||
open-folder = افتح مجلَّد
|
||||
subtitles = الترجمة المرئية
|
||||
unknown-author = مؤلف مجهول
|
||||
untitled = بلا عنوان
|
||||
|
||||
|
|
@ -31,11 +31,24 @@ file = ملف
|
|||
open-media = افتح الوسائط...
|
||||
open-recent-media = افتح الوسائط الحديثة
|
||||
close-file = أغلِق الملف
|
||||
open-media-folder = فتح مجلد الوسائط...
|
||||
open-recent-media-folder = فتح مجلد الوسائط الحديثة
|
||||
open-media-folder = افتح مجلَّد الوسائط...
|
||||
open-recent-media-folder = افتح مجلِّد الوسائط الحديثة
|
||||
close-media-folder = أغلِق مجلد الوسائط
|
||||
quit = أنهِ
|
||||
off = معطّل
|
||||
clear-recent = امحُ القائمة الحديثة
|
||||
repeat-disabled = التكرار مُعطَّل
|
||||
repeat-track = كرِّر المقطع
|
||||
playback = التشغيل
|
||||
next-frame = الإطار التالي
|
||||
previous-frame = الإطار السابق
|
||||
ab-repeat = تكرار A-B
|
||||
ab-repeat-set-a = تكرار A-B (A)
|
||||
ab-repeat-set-b = تكرار A-B (B)
|
||||
ab-repeat-clear = امحُ تكرار A-B
|
||||
|
||||
# XDG Metadata
|
||||
|
||||
xdg-name = مشغّل وسائط COSMIC
|
||||
xdg-comment = مشغّل وسائط لسطح مكتب COSMIC
|
||||
xdg-keywords = فيلم;مقطع;فيديو;صوت
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
quit = Surt
|
||||
settings = Configuració
|
||||
|
|
@ -22,5 +22,18 @@ close-media-folder = Zavřít složku s médii
|
|||
quit = Ukončit
|
||||
off = Vypnuto
|
||||
clear-recent = Vymazat seznam nedávných
|
||||
repeat-track = Opakovat skladbu
|
||||
repeat-disabled = Opakování zakázáno
|
||||
repeat-track = Opakovat stopu
|
||||
repeat-disabled = Opakování vypnuto
|
||||
playback = Přehrávání
|
||||
next-frame = Další snímek
|
||||
previous-frame = Předchozí snímek
|
||||
ab-repeat = Opakování A-B
|
||||
ab-repeat-set-a = Opakování A-B (A)
|
||||
ab-repeat-set-b = Opakování A-B (B)
|
||||
ab-repeat-clear = Vymazat opakování A-B
|
||||
|
||||
# XDG Metadata
|
||||
|
||||
xdg-name = Přehrávač médií COSMIC
|
||||
xdg-comment = Přehrávač médií pro prostředí COSMIC
|
||||
xdg-keywords = Audio;Film;Hudba;Zvuk;Video;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ light = Hell
|
|||
|
||||
file = Datei
|
||||
open-media = Medium öffnen...
|
||||
open-recent-media = Zuletzt verwendete Medien öffnen
|
||||
open-recent-media = Zuletzt verwendetes Medium öffnen
|
||||
close-file = Datei schließen
|
||||
open-media-folder = Medienordner öffnen...
|
||||
open-recent-media-folder = Ordner der zuletzt verwendeten Medien öffnen
|
||||
|
|
@ -38,3 +38,14 @@ quit = Beenden
|
|||
off = Aus
|
||||
repeat-disabled = Wiederholung deaktiviert
|
||||
repeat-track = Titel wiederholen
|
||||
clear-recent = Liste zuletzt genutzter Einträge leeren
|
||||
playback = Wiedergabe
|
||||
next-frame = Nächstes Einzelbild
|
||||
previous-frame = Vorheriges Einzelbild
|
||||
ab-repeat = A-B-Wiederholung
|
||||
ab-repeat-set-a = A-B-Wiederholung (A)
|
||||
ab-repeat-set-b = A-B-Wiederholung (B)
|
||||
ab-repeat-clear = A-B-Wiederholung löschen
|
||||
xdg-keywords = Audio;Film;Musik;Ton;Video;
|
||||
xdg-name = COSMIC Mediaplayer
|
||||
xdg-comment = Mediaplayer für den COSMIC-Desktop
|
||||
|
|
|
|||
|
|
@ -1,2 +1,36 @@
|
|||
dark = Σκοτεινό
|
||||
light = Φωτεινό
|
||||
dark = Σκουρόχρωμο
|
||||
light = Ανοιχτόχρωμο
|
||||
file = Αρχείο
|
||||
open-file = Άνοιγμα αρχείου
|
||||
open-folder = Άνοιγμα φακέλου
|
||||
settings = Ρυθμίσεις
|
||||
close-file = Κλείσιμο αρχείου
|
||||
subtitles = Υπότιτλοι
|
||||
untitled = Χωρίς τίτλο
|
||||
theme = Θέμα
|
||||
repeat-track = Επανάληψη κομματιού
|
||||
appearance = Εμφάνιση
|
||||
quit = Έξοδος
|
||||
album = Άλμπουμ: { $album }
|
||||
playback = Αναπαραγωγή
|
||||
next-frame = Επόμενο καρέ
|
||||
previous-frame = Προηγούμενο καρέ
|
||||
ab-repeat = Επανάληψη Α-Β
|
||||
ab-repeat-set-a = Επανάληψη Α-Β (Α)
|
||||
ab-repeat-set-b = Επανάληψη A-B (B)
|
||||
open-media = Άνοιγμα πολυμέσων...
|
||||
open-media-folder = Άνοιγμα φακέλου πολυμέσων...
|
||||
unknown-author = Άγνωστος δημιουργός
|
||||
match-desktop = Συμφωνία με την επιφάνεια εργασίας
|
||||
close-media-folder = Κλείσιμο φακέλου πολυμέσων
|
||||
repeat-disabled = Επανάληψη απενεργοποιημένη
|
||||
audio = Κομμάτι ήχου
|
||||
off = Ανενεργό
|
||||
open-recent-media = Άνοιγμα πρόσφατων πολυμέσων
|
||||
clear-recent = Απαλοιφή λίστας πρόσφατων
|
||||
no-video-or-audio-file-open = Κανένα ανοικτό αρχείο βίντεο ή ήχου
|
||||
open-recent-media-folder = Άνοιγμα φακέλου πρόσφατων πολυμέσων
|
||||
ab-repeat-clear = Απαλοιφή επανάληψης Α-Β
|
||||
xdg-name = Αναπαραγωγή πολυμέσων COSMIC
|
||||
xdg-keywords = Ήχος;Ταινία;Μουσική;Τραγούδι;Βίντεο;Πολυμέσα;Audio;Film;Movie;Music;Sound;Video;
|
||||
xdg-comment = Πρόγραμμα αναπαραγωγής πολυμέσων για το περιβάλλον επιφάνειας εργασίας COSMIC
|
||||
|
|
|
|||
|
|
@ -45,3 +45,9 @@ ab-repeat = A-B Repeat
|
|||
ab-repeat-set-a = A-B Repeat (A)
|
||||
ab-repeat-set-b = A-B Repeat (B)
|
||||
ab-repeat-clear = Clear A-B Repeat
|
||||
|
||||
# XDG Metadata
|
||||
|
||||
xdg-name = COSMIC Media Player
|
||||
xdg-comment = Media player for the COSMIC desktop
|
||||
xdg-keywords = Audio;Film;Movie;Music;Sound;Video;
|
||||
|
|
|
|||
|
|
@ -36,3 +36,9 @@ open-recent-media-folder = Abrir carpetas multimedia recientes
|
|||
close-media-folder = Cerrar carpeta multimedia
|
||||
quit = Salir
|
||||
off = Apagado
|
||||
|
||||
# XDG Metadata
|
||||
|
||||
xdg-name = Reproductor de medios COSMIC
|
||||
xdg-comment = Reproductor de medios para el escritorio COSMIC
|
||||
xdg-keywords = Audio;Película;Música;Vídeo;Sonido;Reproductor;
|
||||
|
|
|
|||
0
i18n/eu/cosmic_player.ftl
Normal file
0
i18n/eu/cosmic_player.ftl
Normal file
|
|
@ -11,7 +11,7 @@ unknown-author = Tuntematon tekijä
|
|||
untitled = Nimetön
|
||||
appearance = Ulkoasu
|
||||
theme = Teema
|
||||
match-desktop = Sovita työpöydän kanssa
|
||||
match-desktop = Sovita työpöytään
|
||||
dark = Tumma
|
||||
light = Vaalea
|
||||
file = Tiedosto
|
||||
|
|
@ -24,3 +24,13 @@ open-media = Avaa media...
|
|||
open-recent-media = Avaa viimeaikainen media
|
||||
repeat-disabled = Kertaus pois käytöstä
|
||||
repeat-track = Kertaa kappale
|
||||
playback = Toisto
|
||||
next-frame = Seuraava kehys
|
||||
previous-frame = Edellinen kehys
|
||||
ab-repeat = A-B-kertaus
|
||||
ab-repeat-set-a = A-B-kertaus (A)
|
||||
ab-repeat-set-b = A-B-kertaus (B)
|
||||
ab-repeat-clear = Tyhjennä A-B-kertaus
|
||||
xdg-name = COSMICin mediasoitin
|
||||
xdg-comment = Mediasoitin COSMIC-työpöydälle
|
||||
xdg-keywords = Audio;Film;Movie;Music;Sound;Video;ääni;filmi;leffa;elokuva;musiikki;
|
||||
|
|
|
|||
|
|
@ -39,3 +39,13 @@ off = Désactivé
|
|||
clear-recent = Effacer la liste récente
|
||||
repeat-disabled = Répétition désactivée
|
||||
repeat-track = Répéter la piste
|
||||
playback = Lecture
|
||||
next-frame = Prochaine image
|
||||
previous-frame = Image précédente
|
||||
ab-repeat = Répétition A-B
|
||||
ab-repeat-set-a = Répétition A-B (A)
|
||||
ab-repeat-set-b = Répétition A-B (B)
|
||||
ab-repeat-clear = Effacer la répétition A-B
|
||||
xdg-name = Lecteur Multimédia COSMIC
|
||||
xdg-comment = Lecteur Multimédia pour le bureau COSMIC
|
||||
xdg-keywords = Audio;Film;Musique;Son;Vidéo;
|
||||
|
|
|
|||
|
|
@ -39,3 +39,10 @@ off = As
|
|||
clear-recent = Glan an liosta le déanaí
|
||||
repeat-disabled = Athsheinm múchta
|
||||
repeat-track = Athsheinm an riain
|
||||
playback = Athsheinm
|
||||
next-frame = An chéad fráma eile
|
||||
previous-frame = An fráma roimhe seo
|
||||
ab-repeat = Athsheinm A-B
|
||||
ab-repeat-set-a = Athsheinm A-B (A)
|
||||
ab-repeat-set-b = Athsheinm A-B (B)
|
||||
ab-repeat-clear = Glan Athsheinm A-B
|
||||
|
|
|
|||
|
|
@ -39,3 +39,16 @@ off = Ki
|
|||
clear-recent = Legutóbbiak listájának törlése
|
||||
repeat-disabled = Ismétlés letiltva
|
||||
repeat-track = Szám ismétlése
|
||||
playback = Lejátszás
|
||||
next-frame = Következő képkocka
|
||||
previous-frame = Előző képkocka
|
||||
ab-repeat = A–B ismétlés
|
||||
ab-repeat-set-a = A–B ismétlés (A)
|
||||
ab-repeat-set-b = A–B ismétlés (B)
|
||||
ab-repeat-clear = A–B ismétlés törlése
|
||||
|
||||
# XDG Metadata
|
||||
|
||||
xdg-name = COSMIC Médialejátszó
|
||||
xdg-comment = Médialejátszó a COSMIC asztali környezethez
|
||||
xdg-keywords = hang;film;zene;videó;média;lejátszó;
|
||||
|
|
|
|||
|
|
@ -24,3 +24,10 @@ close-media-folder = Tutup map media
|
|||
clear-recent = Hapus daftar terbaru
|
||||
repeat-track = Ulangi trek
|
||||
repeat-disabled = Pengulangan dinonaktifkan
|
||||
ab-repeat-clear = Bersihkan Pengulangan A-B
|
||||
ab-repeat-set-b = Pengulangan A-B (B)
|
||||
ab-repeat-set-a = Pengulangan A-B (A)
|
||||
ab-repeat = Pengulangan A-B
|
||||
playback = Pemutaran
|
||||
next-frame = Bagian Selanjutnya
|
||||
previous-frame = Bagian Sebelumnya
|
||||
|
|
|
|||
|
|
@ -36,3 +36,9 @@ open-recent-media-folder = Apri cartella multimediale recente
|
|||
close-media-folder = Chiudi cartella multimediale
|
||||
quit = Esci
|
||||
off = Off
|
||||
|
||||
# XDG Metadata
|
||||
|
||||
xdg-name = COSMIC Media Player
|
||||
xdg-comment = Lettore multimediale di COSMIC
|
||||
xdg-keywords = Audio;Video;Musica;Film;Suoni;Riproduzione;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
open-file = Ldi afaylu
|
||||
open-folder = Ldi akaram
|
||||
repeat-disabled = Allus yensa
|
||||
repeat-track = Ales tazribt
|
||||
quit = Tuffɣa
|
||||
settings = Iɣewwaṛen
|
||||
appearance = Timeẓri
|
||||
theme = Asentel
|
||||
dark = Aɣmayan
|
||||
light = Aceɛlal
|
||||
match-desktop = Amṣada d tnarit
|
||||
file = Afaylu
|
||||
close-file = Mdel afaylu
|
||||
album = Album: { $album }
|
||||
audio = Ameslaw
|
||||
no-video-or-audio-file-open = Ulac afaylu n uvidyu neɣ n umeslaw yeldin
|
||||
off = Sens
|
||||
subtitles = Iduzwilen
|
||||
unknown-author = Ameskar arussin
|
||||
untitled = War azwel
|
||||
open-media = Ldi amidya...
|
||||
open-recent-media = Ldi amidya n melmi kan
|
||||
clear-recent = Sfeḍ tabdart n melmi kan
|
||||
open-media-folder = Ldi akaram n umidya...
|
||||
open-recent-media-folder = Ldi akaram umidya n melmi kan
|
||||
close-media-folder = Mdel akaram n umidya
|
||||
playback = Taɣuri
|
||||
next-frame = Akatar uḍfir
|
||||
previous-frame = Akatar yezrin
|
||||
ab-repeat = Allus A-B
|
||||
ab-repeat-set-a = Allus A-B (A)
|
||||
ab-repeat-set-b = Allus A-B (B)
|
||||
ab-repeat-clear = Sfeḍ allus A-B
|
||||
|
|
@ -24,3 +24,13 @@ open-recent-media-folder = Жуырдағы медиа бумасын ашу
|
|||
close-media-folder = Медиа бумасын жабу
|
||||
repeat-disabled = Қайталау сөндірілген
|
||||
repeat-track = Тректі қайталау
|
||||
playback = Ойнату
|
||||
next-frame = Келесі фрейм
|
||||
previous-frame = Алдыңғы фрейм
|
||||
ab-repeat = Қайталау А-Ә
|
||||
ab-repeat-set-a = Қайталау А-Ә (А бапт.)
|
||||
ab-repeat-set-b = Қайталау А-Ә (В бапт.)
|
||||
ab-repeat-clear = Қайталау А-Ә (Тазарту)
|
||||
xdg-name = COSMIC медиаплеері
|
||||
xdg-comment = COSMIC жұмыс үстелі үшін медиаплеері
|
||||
xdg-keywords = Audio;Film;Movie;Music;Sound;Video;Аудио;Фильм;Кино;Музыка;Дыбыс;Видео;
|
||||
|
|
|
|||
|
|
@ -22,3 +22,6 @@ no-video-or-audio-file-open = 비디오 혹은 오디오가 열려있지 않음
|
|||
off = 끄기
|
||||
album = 앨범: { $album }
|
||||
close-file = 파일 닫기
|
||||
repeat-disabled = 반복 비활성화
|
||||
previous-frame = 이전 프레임
|
||||
next-frame = 다음 프레임
|
||||
|
|
|
|||
0
i18n/lo/cosmic_player.ftl
Normal file
0
i18n/lo/cosmic_player.ftl
Normal file
|
|
@ -1,5 +1,5 @@
|
|||
quit = Išeiti
|
||||
settings = Nustatymai
|
||||
settings = Nuostatos
|
||||
close-file = Uždaryti failą
|
||||
match-desktop = Pagal darbalaukio temą
|
||||
appearance = Išvaizda
|
||||
|
|
@ -24,3 +24,10 @@ dark = Tamsus
|
|||
light = Šviesus
|
||||
repeat-track = Kartoti takelį
|
||||
repeat-disabled = Kartojimas išjungtas
|
||||
playback = Atkūrimas
|
||||
next-frame = Sekantis Kadras
|
||||
previous-frame = Ankstenis Kadras
|
||||
ab-repeat = A-B Kartojimas
|
||||
ab-repeat-set-a = A-B Kartojimas (A)
|
||||
ab-repeat-set-b = A-B Kartojimas (B)
|
||||
ab-repeat-clear = Panaikinti A-B Kartojimą
|
||||
|
|
|
|||
|
|
@ -39,3 +39,16 @@ off = Wyłączone
|
|||
clear-recent = Wyczyść listę poprzednich
|
||||
repeat-disabled = Powtarzanie wyłączone
|
||||
repeat-track = Powtórz utwór
|
||||
playback = Odtwarzanie
|
||||
next-frame = Następna Klatka
|
||||
ab-repeat = A-B Powtarzanie
|
||||
ab-repeat-set-a = A-B Powtarzanie (A)
|
||||
ab-repeat-set-b = A-B Powtarzanie (B)
|
||||
ab-repeat-clear = Wyczyść Powtarzanie A-B
|
||||
previous-frame = Poprzednia Klatka
|
||||
|
||||
# XDG Metadata
|
||||
|
||||
xdg-name = Odtwarzacz Multimediów COSMIC
|
||||
xdg-comment = Odtwarzacz Multimediów dla środowiska graficznego COSMIC
|
||||
xdg-keywords = Audio;Dźwięki;Filmy;Muzyka;Vídeo;Media;Odtwarzacz;Multimedia;
|
||||
|
|
|
|||
|
|
@ -39,3 +39,16 @@ off = Desligado
|
|||
clear-recent = Limpar lista de recentes
|
||||
repeat-track = Repetir faixa
|
||||
repeat-disabled = Repetição desabilitada
|
||||
next-frame = Próximo quadro
|
||||
previous-frame = Quadro anterior
|
||||
playback = Reprodução
|
||||
ab-repeat = Repetir A-B
|
||||
ab-repeat-set-a = Repetir A-B (A)
|
||||
ab-repeat-set-b = Repetir A-B (B)
|
||||
ab-repeat-clear = Limpar repetição A-B
|
||||
|
||||
# XDG Metadata
|
||||
|
||||
xdg-name = Reprodutor de mídia
|
||||
xdg-comment = Reprodutor de mídia do ambiente COSMIC
|
||||
xdg-keywords = Áudio;Filme;Música;Som;Vídeo;Media;Player;
|
||||
|
|
|
|||
|
|
@ -35,3 +35,9 @@ open-media-folder = Abrir pasta de multimédias...
|
|||
open-recent-media-folder = Abrir pasta recente de multimédias
|
||||
close-media-folder = Fechar pasta de multimédia
|
||||
quit = Sair
|
||||
|
||||
# XDG Metadata
|
||||
|
||||
xdg-name = Reprodutor de Mídia
|
||||
xdg-comment = Reprodutor de mídia do desktop COSMIC
|
||||
xdg-keywords = Audio;Filme;Música;Som;Vídeo;Media;Player;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ light = Светлая
|
|||
open-file = Открыть файл
|
||||
album = Альбом: { $album }
|
||||
audio = Аудио
|
||||
no-video-or-audio-file-open = Нет открытых аудио- или видеофайлов
|
||||
no-video-or-audio-file-open = Нет открытых видео или аудио файлов
|
||||
open-folder = Открыть папку
|
||||
subtitles = Субтитры
|
||||
unknown-author = Неизвестный автор
|
||||
|
|
@ -24,11 +24,24 @@ file = Файл
|
|||
open-media = Открыть медиафайл…
|
||||
open-recent-media = Открыть недавний медиафайл
|
||||
close-file = Закрыть файл
|
||||
open-media-folder = Открыть папку медиафайлов…
|
||||
open-recent-media-folder = Недавние папки медиафайлов
|
||||
open-media-folder = Открыть папку медиафайлов...
|
||||
open-recent-media-folder = Открыть папку недавних медиафайлов
|
||||
close-media-folder = Закрыть папку медиафайлов
|
||||
quit = Выйти
|
||||
off = Выкл.
|
||||
clear-recent = Очистить список недавних
|
||||
repeat-disabled = Повтор отключён
|
||||
repeat-track = Повторять трек
|
||||
playback = Воспроизведение
|
||||
next-frame = Следующий кадр
|
||||
previous-frame = Предыдущий кадр
|
||||
ab-repeat = Повтор A–B
|
||||
ab-repeat-set-a = Повтор A–B (уст. A)
|
||||
ab-repeat-set-b = Повтор A–B (уст. B)
|
||||
ab-repeat-clear = Повтор A–B (очистить)
|
||||
|
||||
# XDG Metadata
|
||||
|
||||
xdg-name = Медиаплеер
|
||||
xdg-keywords = Аудио;Фильм;Музыка;Видео;Медиа;Звук;Мультимедиа;Медиаплеер;
|
||||
xdg-comment = Медиаплеер для рабочей среды COSMIC
|
||||
|
|
|
|||
|
|
@ -30,3 +30,9 @@ open-media-folder = Otvoriť priečinok s médiami...
|
|||
open-recent-media-folder = Otvoriť posledný priečinok s médiami
|
||||
close-media-folder = Zatvoriť priečinok s médiami
|
||||
quit = Ukončiť
|
||||
|
||||
# XDG Metadata
|
||||
|
||||
xdg-name = Prehrávač Médií COSMIC
|
||||
xdg-comment = Prehrávač médií pre pracovné prostredie COSMIC
|
||||
xdg-keywords = audio;film;hudba;video;prehrávač;média;zvuk;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
open-file = Отвори датотеку
|
||||
quit = Изађи
|
||||
settings = Подешавања
|
||||
appearance = Изглед
|
||||
dark = Тамна
|
||||
light = Светла
|
||||
open-folder = Отвори фасциклу
|
||||
theme = Тема
|
||||
match-desktop = Прати радну површину
|
||||
file = Датотека
|
||||
close-file = Затвори датотеку
|
||||
album = Албум: { $album }
|
||||
audio = Звук
|
||||
no-video-or-audio-file-open = Нема отворене датотеке видеа или звука
|
||||
off = Искључено
|
||||
subtitles = Поднаслови
|
||||
unknown-author = Непознати аутор
|
||||
untitled = Без наслова
|
||||
open-media = Отвори медиј...
|
||||
open-recent-media = Отвори недавне медије
|
||||
clear-recent = Очисти списак недавних
|
||||
open-media-folder = Отвори медијску фасциклу...
|
||||
open-recent-media-folder = Отвори недавну медијску фасциклу
|
||||
close-media-folder = Затвори медијску фасциклу
|
||||
repeat-disabled = Понављање онемогућено
|
||||
repeat-track = Понови песму
|
||||
playback = Пуштање
|
||||
next-frame = Следећи кадар
|
||||
previous-frame = Претходни кадар
|
||||
ab-repeat = Понављање А-Б
|
||||
ab-repeat-set-a = А-Б понављање (А)
|
||||
ab-repeat-set-b = А-Б понављање (Б)
|
||||
ab-repeat-clear = Очисти А-Б понављање
|
||||
xdg-name = Космик пуштач медија
|
||||
xdg-comment = Пуштач медија за Космик радну површ
|
||||
xdg-keywords = Audio;Film;Movie;Music;Sound;Video; Аудио;Звук;Филм;Музика;Видео;zvuk;film;muzika;
|
||||
|
|
@ -39,3 +39,16 @@ off = Av
|
|||
clear-recent = Töm senaste-listan
|
||||
repeat-disabled = Upprepning avaktiverad
|
||||
repeat-track = Upprepa spår
|
||||
playback = Uppspelning
|
||||
next-frame = Nästa bildruta
|
||||
previous-frame = Föregående bildruta
|
||||
ab-repeat = A-B-upprepning
|
||||
ab-repeat-set-a = A-B-upprepning (A)
|
||||
ab-repeat-set-b = A-B-upprepning (B)
|
||||
ab-repeat-clear = Rensa A-B-upprepning
|
||||
|
||||
# XDG Metadata
|
||||
|
||||
xdg-name = COSMIC Mediaspelare
|
||||
xdg-comment = Mediaspelare för skrivbordsmiljön COSMIC
|
||||
xdg-keywords = Ljud;Film;Musik;Video;Media;
|
||||
|
|
|
|||
|
|
@ -13,14 +13,24 @@ match-desktop = Системна
|
|||
dark = Темна
|
||||
light = Світла
|
||||
file = Файл
|
||||
open-media = Відкрити медіа...
|
||||
open-recent-media = Відкрити останні медіа
|
||||
open-media = Відкрити...
|
||||
open-recent-media = Відкрити попередні
|
||||
close-file = Закрити файл
|
||||
open-media-folder = Відкрити теку з медіа...
|
||||
open-recent-media-folder = Відкрити нещодавню теку з медіа
|
||||
close-media-folder = Закрити теку з медіа
|
||||
open-media-folder = Відкрити теку...
|
||||
open-recent-media-folder = Відкрити попередню теку
|
||||
close-media-folder = Закрити теку
|
||||
quit = Вийти
|
||||
off = Вимк.
|
||||
clear-recent = Очистити список
|
||||
repeat-disabled = Не повторювати
|
||||
repeat-track = Повторювати
|
||||
previous-frame = Попередній кадр
|
||||
next-frame = Наступний кадр
|
||||
xdg-name = Програвач COSMIC
|
||||
xdg-comment = Програвач для стільниці COSMIC
|
||||
xdg-keywords = Аудіо;Стрічка;Фільм;Кіно;Музика;Звук;Відео;
|
||||
playback = Програти
|
||||
ab-repeat = Повторення A-B
|
||||
ab-repeat-set-a = Повторення A-B (A)
|
||||
ab-repeat-set-b = Повторення A-B (B)
|
||||
ab-repeat-clear = Очистити повтор A-B
|
||||
|
|
|
|||
|
|
@ -28,10 +28,10 @@ light = 亮色模式
|
|||
## File
|
||||
|
||||
file = 文件
|
||||
open-media = 打开媒体文件...
|
||||
open-media = 打开媒体文件…
|
||||
open-recent-media = 打开最近媒体文件
|
||||
close-file = 关闭文件
|
||||
open-media-folder = 打开媒体文件夹...
|
||||
open-media-folder = 打开媒体文件夹…
|
||||
open-recent-media-folder = 打开最近媒体文件夹
|
||||
close-media-folder = 关闭媒体文件夹
|
||||
quit = 退出
|
||||
|
|
@ -39,3 +39,16 @@ off = 已关闭
|
|||
clear-recent = 清空近期列表
|
||||
repeat-disabled = 重复已禁用
|
||||
repeat-track = 重复轨道
|
||||
playback = 回放
|
||||
next-frame = 下一帧
|
||||
previous-frame = 上一帧
|
||||
ab-repeat = A-B 重复
|
||||
ab-repeat-set-a = A-B 重复(A)
|
||||
ab-repeat-set-b = A-B 重复(B)
|
||||
ab-repeat-clear = 清除 A-B 重复
|
||||
|
||||
# XDG Metadata
|
||||
|
||||
xdg-name = COSMIC 媒体播放器
|
||||
xdg-comment = COSMIC 桌面的媒体播放器
|
||||
xdg-keywords = 音频;影片;电影;音乐;声音;视频;
|
||||
|
|
|
|||
|
|
@ -1,32 +1,51 @@
|
|||
album = 專輯: {$album}
|
||||
album = 專輯:{ $album }
|
||||
audio = 音訊
|
||||
no-video-or-audio-file-open = 未開啟視訊或音訊檔案
|
||||
no-video-or-audio-file-open = 無影片或音訊檔案開啟
|
||||
open-file = 開啟檔案
|
||||
open-folder = 打開資料夾
|
||||
open-folder = 開啟資料夾
|
||||
subtitles = 字幕
|
||||
unknown-author = 未知作者
|
||||
unknown-author = 不明作者
|
||||
untitled = 無標題
|
||||
|
||||
# Context Pages
|
||||
|
||||
|
||||
## Settings
|
||||
|
||||
settings = 設定
|
||||
|
||||
### Appearance
|
||||
|
||||
appearance = 外觀
|
||||
theme = 主題
|
||||
match-desktop = 與桌面保持一致
|
||||
dark = 深色主題
|
||||
light = 淺色主題
|
||||
match-desktop = 符合桌面
|
||||
dark = 深色
|
||||
light = 淺色
|
||||
|
||||
# Menu
|
||||
|
||||
|
||||
## File
|
||||
|
||||
file = 檔案
|
||||
open-media = 開啟媒體檔案...
|
||||
open-recent-media = 打開最近的媒體檔案
|
||||
open-media = 開啟媒體...
|
||||
open-recent-media = 開啟最近使用的媒體
|
||||
close-file = 關閉檔案
|
||||
open-media-folder = 開啟媒體資料夾...
|
||||
open-recent-media-folder = 開啟最近的媒體資料夾
|
||||
open-recent-media-folder = 開啟最近使用的媒體資料夾
|
||||
close-media-folder = 關閉媒體資料夾
|
||||
quit = 退出
|
||||
quit = 退出
|
||||
off = 關閉
|
||||
clear-recent = 清除最近使用清單
|
||||
repeat-disabled = 重複已經停用
|
||||
repeat-track = 重複曲目
|
||||
ab-repeat = A-B 重複
|
||||
ab-repeat-set-a = A-B 重複(A)
|
||||
ab-repeat-set-b = A-B 重複(B)
|
||||
ab-repeat-clear = 清除 A-B 重複
|
||||
playback = 回放
|
||||
next-frame = 下一幀
|
||||
previous-frame = 上一幀
|
||||
xdg-name = COSMIC 媒體播放器
|
||||
xdg-comment = 媒體播放器 用於 COSMIC 桌面
|
||||
xdg-keywords = 音訊;影片;電影;音樂;聲音;影像;
|
||||
|
|
|
|||
4
justfile
4
justfile
|
|
@ -13,11 +13,11 @@ bin-src := cargo-target-dir / 'release' / name
|
|||
bin-dst := base-dir / 'bin' / name
|
||||
|
||||
desktop := APPID + '.desktop'
|
||||
desktop-src := 'res' / desktop
|
||||
desktop-src := 'target/xdgen' / desktop
|
||||
desktop-dst := clean(rootdir / prefix) / 'share' / 'applications' / desktop
|
||||
|
||||
metainfo := APPID + '.metainfo.xml'
|
||||
metainfo-src := 'res' / metainfo
|
||||
metainfo-src := 'target/xdgen' / metainfo
|
||||
metainfo-dst := clean(rootdir / prefix) / 'share' / 'metainfo' / metainfo
|
||||
|
||||
thumbnailer := APPID + '.thumbnailer'
|
||||
|
|
|
|||
|
|
@ -1,18 +1,6 @@
|
|||
#TODO: more build-out, desktop actions, translations?
|
||||
[Desktop Entry]
|
||||
Name=COSMIC Media Player
|
||||
Name[ar]=مشغّل وسائط COSMIC
|
||||
Name[cs]=Přehrávač médií COSMIC
|
||||
Name[zh_CN]=COSMIC 媒体播放器
|
||||
Name[hu]=COSMIC Médialejátszó
|
||||
Name[pl]=Odtwarzacz Multimediów COSMIC
|
||||
Name[pt_BR]=Reprodutor de Mídia
|
||||
Name[pt]=Reprodutor de Mídia
|
||||
Name[ru]=Медиаплеер
|
||||
Name[sk]=Prehrávač Médií COSMIC
|
||||
Name[sv]=COSMIC Mediaspelare
|
||||
Name[es]=Reproductor de medios COSMIC
|
||||
Name[it]=COSMIC Media Player
|
||||
Comment=Media player for the COSMIC desktop
|
||||
Exec=cosmic-player %U
|
||||
Terminal=false
|
||||
Type=Application
|
||||
|
|
@ -21,14 +9,3 @@ MimeType=application/mxf;application/ram;application/sdp;application/vnd.apple.m
|
|||
Icon=com.system76.CosmicPlayer
|
||||
Categories=COSMIC;AudioVideo;Player;Video;
|
||||
Keywords=Audio;Film;Movie;Music;Sound;Video;
|
||||
Keywords[ar]=فيلم;مقطع;فيديو;صوت
|
||||
Keywords[cs]=audio;film;hudba;video;média;zvuk;
|
||||
Keywords[hu]=hang;film;zene;videó;média;lejátszó;
|
||||
Keywords[pl]=Audio;Dźwięki;Filmy;Muzyka;Vídeo;Media;Odtwarzacz;Multimedia;
|
||||
Keywords[pt_BR]=Audio;Filme;Música;Som;Vídeo;Media;Player;
|
||||
Keywords[ru]=Аудио;Фильм;Музыка;Видео;Медиа;Звук;Мультимедиа;Медиаплеер;
|
||||
Keywords[pt]=Audio;Filme;Música;Som;Vídeo;Media;Player;
|
||||
Keywords[sk]=audio;film;hudba;video;prehrávač;média;zvuk;
|
||||
Keywords[sv]=Ljud;Film;Musik;Video;Media;
|
||||
Keywords[es]=Audio;Película;Música;Vídeo;Sonido;Reproductor;
|
||||
Keywords[it]=Audio;Video;Musica;Film;Suoni;Riproduzione;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<component type="desktop-application">
|
||||
<id>com.system76.CosmicPlayer</id>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
|
|
@ -9,33 +9,15 @@
|
|||
<url type="homepage">https://github.com/pop-os/cosmic-player</url>
|
||||
<url type="bugtracker">https://github.com/pop-os/cosmic-player</url>
|
||||
<name>COSMIC Media Player</name>
|
||||
<name xml:lang="ar">مشغّل وسائط COSMIC</name>
|
||||
<name xml:lang="cs">Přehrávač médií COSMIC</name>
|
||||
<name xml:lang="hu">COSMIC Médialejátszó</name>
|
||||
<name xml:lang="pl">Odtwarzacz Multimediów COSMIC</name>
|
||||
<name xml:lang="pt_BR">Reprodutor de Mídia</name>
|
||||
<name xml:lang="pt">Reprodutor de Mídia</name>
|
||||
<name xml:lang="sk">Prehrávač Médií COSMIC</name>
|
||||
<name xml:lang="es">Reproductor de medios COSMIC</name>
|
||||
<name xml:lang="it">COSMIC Media Player</name>
|
||||
<name xml:lang="sv">COSMIC Mediaspelare</name>
|
||||
<summary>Media player for the COSMIC desktop</summary>
|
||||
<summary xml:lang="ar">مشغّل وسائط لسطح مكتب COSMIC</summary>
|
||||
<summary xml:lang="cs">Přehrávač médií pro pracovní prostředí COSMIC</summary>
|
||||
<summary xml:lang="hu">Médialejátszó a COSMIC asztali környezethez</summary>
|
||||
<summary xml:lang="pl">Odtwarzacz Multimediów dla środowiska graficznego COSMIC</summary>
|
||||
<summary xml:lang="pt_BR">Reprodutor de mídia do desktop COSMIC</summary>
|
||||
<summary xml:lang="pt">Reprodutor de mídia do desktop COSMIC</summary>
|
||||
<summary xml:lang="sk">Prehrávač médií pre pracovné prostredie COSMIC</summary>
|
||||
<summary xml:lang="es">Reproductor de medios para el escritorio COSMIC</summary>
|
||||
<summary xml:lang="it">Lettore multimediale di COSMIC</summary>
|
||||
<summary xml:lang="sv">Mediaspelare för skrivbordsmiljön COSMIC</summary>
|
||||
<description>
|
||||
<p>Media player for the COSMIC desktop</p>
|
||||
<p xml:lang="ar">مشغّل وسائط لسطح مكتب COSMIC</p>
|
||||
<p xml:lang="cs">Přehrávač médií pro pracovní prostředí COSMIC</p>
|
||||
<p xml:lang="hu">Médialejátszó a COSMIC asztali környezethez</p>
|
||||
<p xml:lang="pl">Odtwarzacz Multimediów dla środowiska graficznego COSMIC</p>
|
||||
<p
|
||||
xml:lang="pl"
|
||||
>Odtwarzacz Multimediów dla środowiska graficznego COSMIC</p>
|
||||
<p xml:lang="pt_BR">Reprodutor de mídia do desktop COSMIC</p>
|
||||
<p xml:lang="pt">Reprodutor de mídia do desktop COSMIC</p>
|
||||
<p xml:lang="sk">Prehrávač médií pre pracovné prostredie COSMIC</p>
|
||||
|
|
@ -43,8 +25,20 @@
|
|||
<p xml:lang="es">Reproductor de medios para el escritorio COSMIC</p>
|
||||
<p xml:lang="sv">Mediaspelare för skrivbordsmiljön COSMIC</p>
|
||||
</description>
|
||||
<keywords>
|
||||
<keyword>Audio</keyword>
|
||||
<keyword>Film</keyword>
|
||||
<keyword>Movie</keyword>
|
||||
<keyword>Music</keyword>
|
||||
<keyword>Sound</keyword>
|
||||
<keyword>Video</keyword>
|
||||
</keywords>
|
||||
<launchable type="desktop-id">com.system76.CosmicPlayer.desktop</launchable>
|
||||
<icon type="remote" height="256" width="256">https://raw.githubusercontent.com/pop-os/cosmic-player/master/res/icons/hicolor/256x256/apps/com.system76.CosmicPlayer.svg</icon>
|
||||
<icon
|
||||
type="remote"
|
||||
height="256"
|
||||
width="256"
|
||||
>https://raw.githubusercontent.com/pop-os/cosmic-player/master/res/icons/hicolor/256x256/apps/com.system76.CosmicPlayer.svg</icon>
|
||||
<provides>
|
||||
<id>com.system76.CosmicApplication</id>
|
||||
<binaries>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.1693 13.9167C3.11349 13.9167 3.06326 13.8972 3.01862 13.8583C2.97955 13.8194 2.96002 13.7694 2.96002 13.7083V9.56667L1.75459 10.4917C1.70436 10.5306 1.65135 10.5444 1.59554 10.5333C1.54531 10.5222 1.50067 10.4917 1.4616 10.4417L1.04305 9.9C1.00957 9.85 0.995615 9.79722 1.0012 9.74167C1.01236 9.68611 1.04305 9.64167 1.09328 9.60833L3.00187 8.14167C3.03536 8.11944 3.06605 8.10556 3.09396 8.1C3.12744 8.08889 3.16372 8.08333 3.20278 8.08333H4.09011C4.14592 8.08333 4.19335 8.10278 4.23242 8.14167C4.27148 8.18056 4.29102 8.23056 4.29102 8.29167V13.7083C4.29102 13.7694 4.27148 13.8194 4.23242 13.8583C4.19335 13.8972 4.14592 13.9167 4.09011 13.9167H3.1693Z" fill="#232323"/>
|
||||
<path d="M7.66029 14C7.25848 14 6.91248 13.9444 6.62228 13.8333C6.33767 13.7167 6.10049 13.5583 5.91075 13.3583C5.72658 13.1528 5.58707 12.9167 5.49219 12.65C5.4029 12.3778 5.34989 12.0889 5.33314 11.7833C5.32756 11.6333 5.32198 11.4667 5.3164 11.2833C5.3164 11.0944 5.3164 10.9056 5.3164 10.7167C5.32198 10.5278 5.32756 10.3556 5.33314 10.2C5.34431 9.89444 5.39732 9.60833 5.49219 9.34167C5.59265 9.075 5.73774 8.84167 5.92749 8.64167C6.11723 8.44167 6.35441 8.28611 6.63903 8.175C6.92922 8.05833 7.26965 8 7.66029 8C8.05094 8 8.38857 8.05833 8.67319 8.175C8.95781 8.28611 9.19499 8.44167 9.38473 8.64167C9.57447 8.84167 9.71678 9.075 9.81165 9.34167C9.9121 9.60833 9.9707 9.89444 9.98744 10.2C9.99302 10.3556 9.99581 10.5278 9.99581 10.7167C10.0014 10.9056 10.0014 11.0944 9.99581 11.2833C9.99581 11.4667 9.99302 11.6333 9.98744 11.7833C9.9707 12.0889 9.91489 12.3778 9.82002 12.65C9.72515 12.9167 9.58284 13.1528 9.3931 13.3583C9.20894 13.5583 8.97176 13.7167 8.68156 13.8333C8.39695 13.9444 8.05652 14 7.66029 14ZM7.66029 12.9167C7.98397 12.9167 8.22115 12.8111 8.37183 12.6C8.52251 12.3889 8.60343 12.1028 8.61459 11.7417C8.62017 11.5806 8.62296 11.4139 8.62296 11.2417C8.62854 11.0694 8.62854 10.8972 8.62296 10.725C8.62296 10.5528 8.62017 10.3917 8.61459 10.2417C8.60343 9.89722 8.52251 9.61667 8.37183 9.4C8.22115 9.18333 7.98397 9.07222 7.66029 9.06667C7.33103 9.07222 7.09106 9.18333 6.94038 9.4C6.79529 9.61667 6.71716 9.89722 6.706 10.2417C6.70041 10.3917 6.69483 10.5528 6.68925 10.725C6.68925 10.8972 6.68925 11.0694 6.68925 11.2417C6.69483 11.4139 6.70041 11.5806 6.706 11.7417C6.71716 12.1028 6.79808 12.3889 6.94876 12.6C7.09943 12.8111 7.33661 12.9167 7.66029 12.9167Z" fill="#232323"/>
|
||||
<path d="M1.5 4L6 7V5.01465C6.16505 5.00595 6.33174 5 6.5 5C10.0899 5 13 7.01472 13 9.5C13 10.4041 12.613 11.244 11.9502 11.9492C11.6152 12.3713 11.6558 12.9781 12.0479 13.3281C12.455 13.6916 13.1076 13.6232 13.4854 13.2002C14.4387 12.1498 15 10.8755 15 9.5C15 5.91015 11.1944 3 6.5 3C6.33214 3 6.16545 3.00438 6 3.01172V1L1.5 4Z" fill="#232323"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.1693 13.9167C7.11349 13.9167 7.06326 13.8972 7.01862 13.8583C6.97955 13.8194 6.96002 13.7694 6.96002 13.7083V9.56667L5.75459 10.4917C5.70436 10.5306 5.65135 10.5444 5.59554 10.5333C5.54531 10.5222 5.50067 10.4917 5.4616 10.4417L5.04305 9.9C5.00957 9.85 4.99562 9.79722 5.0012 9.74167C5.01236 9.68611 5.04305 9.64167 5.09328 9.60833L7.00187 8.14167C7.03536 8.11944 7.06605 8.10556 7.09396 8.1C7.12744 8.08889 7.16372 8.08333 7.20278 8.08333H8.09011C8.14592 8.08333 8.19335 8.10278 8.23242 8.14167C8.27148 8.18056 8.29102 8.23056 8.29102 8.29167V13.7083C8.29102 13.7694 8.27148 13.8194 8.23242 13.8583C8.19335 13.8972 8.14592 13.9167 8.09011 13.9167H7.1693Z" fill="#232323"/>
|
||||
<path d="M11.6603 14C11.2585 14 10.9125 13.9444 10.6223 13.8333C10.3377 13.7167 10.1005 13.5583 9.91075 13.3583C9.72658 13.1528 9.58707 12.9167 9.49219 12.65C9.4029 12.3778 9.34989 12.0889 9.33314 11.7833C9.32756 11.6333 9.32198 11.4667 9.3164 11.2833C9.3164 11.0944 9.3164 10.9056 9.3164 10.7167C9.32198 10.5278 9.32756 10.3556 9.33314 10.2C9.34431 9.89444 9.39732 9.60833 9.49219 9.34167C9.59265 9.075 9.73774 8.84167 9.92749 8.64167C10.1172 8.44167 10.3544 8.28611 10.639 8.175C10.9292 8.05833 11.2696 8 11.6603 8C12.0509 8 12.3886 8.05833 12.6732 8.175C12.9578 8.28611 13.195 8.44167 13.3847 8.64167C13.5745 8.84167 13.7168 9.075 13.8117 9.34167C13.9121 9.60833 13.9707 9.89444 13.9874 10.2C13.993 10.3556 13.9958 10.5278 13.9958 10.7167C14.0014 10.9056 14.0014 11.0944 13.9958 11.2833C13.9958 11.4667 13.993 11.6333 13.9874 11.7833C13.9707 12.0889 13.9149 12.3778 13.82 12.65C13.7252 12.9167 13.5828 13.1528 13.3931 13.3583C13.2089 13.5583 12.9718 13.7167 12.6816 13.8333C12.3969 13.9444 12.0565 14 11.6603 14ZM11.6603 12.9167C11.984 12.9167 12.2212 12.8111 12.3718 12.6C12.5225 12.3889 12.6034 12.1028 12.6146 11.7417C12.6202 11.5806 12.623 11.4139 12.623 11.2417C12.6285 11.0694 12.6285 10.8972 12.623 10.725C12.623 10.5528 12.6202 10.3917 12.6146 10.2417C12.6034 9.89722 12.5225 9.61667 12.3718 9.4C12.2212 9.18333 11.984 9.07222 11.6603 9.06667C11.331 9.07222 11.0911 9.18333 10.9404 9.4C10.7953 9.61667 10.7172 9.89722 10.706 10.2417C10.7004 10.3917 10.6948 10.5528 10.6893 10.725C10.6893 10.8972 10.6893 11.0694 10.6893 11.2417C10.6948 11.4139 10.7004 11.5806 10.706 11.7417C10.7172 12.1028 10.7981 12.3889 10.9488 12.6C11.0994 12.8111 11.3366 12.9167 11.6603 12.9167Z" fill="#232323"/>
|
||||
<path d="M14.5 4L10 7V5.01465C9.83495 5.00595 9.66826 5 9.5 5C5.91015 5 3 7.01472 3 9.5C3 10.4041 3.38701 11.244 4.0498 11.9492C4.38477 12.3713 4.34421 12.9781 3.95215 13.3281C3.545 13.6916 2.89241 13.6232 2.51465 13.2002C1.56125 12.1498 1 10.8755 1 9.5C1 5.91015 4.80558 3 9.5 3C9.66786 3 9.83455 3.00438 10 3.01172V1L14.5 4Z" fill="#232323"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
|
@ -1 +1 @@
|
|||
reorder_imports = false
|
||||
imports_granularity = "Module"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{fs, io, path::PathBuf};
|
||||
use std::path::PathBuf;
|
||||
use std::{fs, io};
|
||||
|
||||
use clap_lex::RawArgs;
|
||||
use log::warn;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::{
|
||||
cosmic_config::{self, CosmicConfigEntry, cosmic_config_derive::CosmicConfigEntry},
|
||||
theme,
|
||||
};
|
||||
use cosmic::cosmic_config::cosmic_config_derive::CosmicConfigEntry;
|
||||
use cosmic::cosmic_config::{self, CosmicConfigEntry};
|
||||
use cosmic::theme;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::VecDeque, path::PathBuf};
|
||||
use std::collections::VecDeque;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub const CONFIG_VERSION: u64 = 1;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use cosmic::{iced::keyboard::Key, iced::core::keyboard::key::Named};
|
||||
use cosmic::iced::keyboard::Key;
|
||||
use cosmic::iced::keyboard::key::Named;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::Action;
|
||||
|
|
|
|||
|
|
@ -1,50 +1,64 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use i18n_embed::{
|
||||
DefaultLocalizer, LanguageLoader, Localizer,
|
||||
fluent::{FluentLanguageLoader, fluent_language_loader},
|
||||
};
|
||||
use icu_collator::{Collator, CollatorOptions, Numeric};
|
||||
use icu_provider::DataLocale;
|
||||
use i18n_embed::fluent::{FluentLanguageLoader, fluent_language_loader};
|
||||
use i18n_embed::{DefaultLocalizer, LanguageLoader, Localizer};
|
||||
use icu_collator::options::CollatorOptions;
|
||||
use icu_collator::preferences::CollationNumericOrdering;
|
||||
use icu_collator::{Collator, CollatorBorrowed, CollatorPreferences};
|
||||
use icu_locale::Locale;
|
||||
use rust_embed::RustEmbed;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "i18n/"]
|
||||
struct Localizations;
|
||||
|
||||
pub static LANGUAGE_LOADER: OnceLock<FluentLanguageLoader> = OnceLock::new();
|
||||
pub static LANGUAGE_SORTER: OnceLock<Collator> = OnceLock::new();
|
||||
pub static LANGUAGE_LOADER: LazyLock<FluentLanguageLoader> = LazyLock::new(|| {
|
||||
let loader: FluentLanguageLoader = fluent_language_loader!();
|
||||
|
||||
loader
|
||||
.load_fallback_language(&Localizations)
|
||||
.expect("Error while loading fallback language");
|
||||
|
||||
loader
|
||||
});
|
||||
|
||||
pub static LANGUAGE_SORTER: LazyLock<CollatorBorrowed> = LazyLock::new(|| {
|
||||
let create_collator = |locale: Locale| {
|
||||
let mut prefs = CollatorPreferences::from(locale);
|
||||
prefs.numeric_ordering = Some(CollationNumericOrdering::True);
|
||||
Collator::try_new(prefs, CollatorOptions::default()).ok()
|
||||
};
|
||||
|
||||
Locale::try_from_str(&LANGUAGE_LOADER.current_language().to_string())
|
||||
.ok()
|
||||
.and_then(create_collator)
|
||||
.or_else(|| {
|
||||
Locale::try_from_str(&LANGUAGE_LOADER.fallback_language().to_string())
|
||||
.ok()
|
||||
.and_then(create_collator)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
let locale = Locale::try_from_str("en-US").expect("en-US is a valid BCP-47 tag");
|
||||
create_collator(locale)
|
||||
.expect("Creating a collator from the system's current language, the fallback language, or American English should succeed")
|
||||
})
|
||||
});
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! fl {
|
||||
($message_id:literal) => {{
|
||||
i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER.get().unwrap(), $message_id)
|
||||
i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id)
|
||||
}};
|
||||
|
||||
($message_id:literal, $($args:expr),*) => {{
|
||||
i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER.get().unwrap(), $message_id, $($args), *)
|
||||
i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id, $($args), *)
|
||||
}};
|
||||
}
|
||||
|
||||
// Get the `Localizer` to be used for localizing this library.
|
||||
pub fn localizer() -> Box<dyn Localizer> {
|
||||
LANGUAGE_LOADER.get_or_init(|| {
|
||||
let loader: FluentLanguageLoader = fluent_language_loader!();
|
||||
|
||||
loader
|
||||
.load_fallback_language(&Localizations)
|
||||
.expect("Error while loading fallback language");
|
||||
|
||||
loader
|
||||
});
|
||||
|
||||
Box::from(DefaultLocalizer::new(
|
||||
LANGUAGE_LOADER.get().unwrap(),
|
||||
&Localizations,
|
||||
))
|
||||
Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations))
|
||||
}
|
||||
|
||||
pub fn localize() {
|
||||
|
|
@ -52,25 +66,6 @@ pub fn localize() {
|
|||
let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages();
|
||||
|
||||
if let Err(error) = localizer.select(&requested_languages) {
|
||||
eprintln!("Error while loading language for App List {}", error);
|
||||
eprintln!("Error while loading language for COSMIC Media Player {error}",);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sorter() -> &'static Collator {
|
||||
LANGUAGE_SORTER.get_or_init(|| {
|
||||
let mut options = CollatorOptions::new();
|
||||
options.numeric = Some(Numeric::On);
|
||||
let localizer = localizer();
|
||||
let language_loader = localizer.language_loader();
|
||||
|
||||
DataLocale::from_str(&language_loader.current_language().to_string())
|
||||
.or_else(|_| DataLocale::from_str(&language_loader.fallback_language().to_string()))
|
||||
.ok()
|
||||
.and_then(|locale| Collator::try_new(&locale, options).ok())
|
||||
.or_else(|| {
|
||||
let locale = DataLocale::from_str("en-US").expect("en-US is a valid BCP-47 tag");
|
||||
Collator::try_new(&locale, options).ok()
|
||||
})
|
||||
.expect("Creating a collator from the system's current language, the fallback language, or American English should succeed")
|
||||
})
|
||||
}
|
||||
|
|
|
|||
259
src/main.rs
259
src/main.rs
|
|
@ -1,49 +1,32 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::{
|
||||
action,
|
||||
app::{Core, Settings, Task},
|
||||
command::set_theme,
|
||||
cosmic_config::{self, CosmicConfigEntry},
|
||||
cosmic_theme, executor, font,
|
||||
iced::{
|
||||
event::{self, Event},
|
||||
keyboard::{Event as KeyEvent, Key, Modifiers},
|
||||
mouse::{Event as MouseEvent, ScrollDelta},
|
||||
window::{self, set_mode},
|
||||
Alignment, Background, Border, Color, ContentFit, Length, Limits, Subscription,
|
||||
},
|
||||
theme,
|
||||
widget::{self, menu::action::MenuAction, nav_bar, segmented_button, Slider},
|
||||
Application, ApplicationExt, Element,
|
||||
};
|
||||
use iced_video_player::{
|
||||
gst::{self, prelude::*},
|
||||
gst_pbutils, Video, VideoPlayer,
|
||||
};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
collections::HashMap,
|
||||
ffi::{CStr, CString},
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
use cosmic::app::{Core, Settings, Task};
|
||||
use cosmic::command::set_theme;
|
||||
use cosmic::cosmic_config::{self, CosmicConfigEntry};
|
||||
use cosmic::iced::event::{self, Event};
|
||||
use cosmic::iced::keyboard::{Event as KeyEvent, Key, Modifiers};
|
||||
use cosmic::iced::mouse::{Event as MouseEvent, ScrollDelta};
|
||||
use cosmic::iced::{
|
||||
Alignment, Background, Border, Color, ContentFit, Length, Limits, Subscription, exit, window,
|
||||
};
|
||||
use cosmic::iced::window::set_mode;
|
||||
use cosmic::widget::menu::action::MenuAction;
|
||||
use cosmic::widget::{self, Slider, nav_bar, segmented_button};
|
||||
use cosmic::{Application, ApplicationExt, Element, action, cosmic_theme, executor, font, theme};
|
||||
use iced_video_player::gst::prelude::*;
|
||||
use iced_video_player::{Video, VideoPlayer, gst, gst_pbutils};
|
||||
use std::any::TypeId;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{fs, process, thread};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::{
|
||||
config::{Config, ConfigState, CONFIG_VERSION, RepeatState},
|
||||
key_bind::{key_binds, KeyBind},
|
||||
mpris::subscription,
|
||||
project::ProjectNode,
|
||||
};
|
||||
use crate::config::{CONFIG_VERSION, Config, ConfigState, RepeatState};
|
||||
use crate::key_bind::{KeyBind, key_binds};
|
||||
use crate::project::ProjectNode;
|
||||
|
||||
mod argparse;
|
||||
mod config;
|
||||
|
|
@ -64,6 +47,12 @@ const GST_PLAY_FLAG_VIDEO: i32 = 1 << 0;
|
|||
const GST_PLAY_FLAG_AUDIO: i32 = 1 << 1;
|
||||
const GST_PLAY_FLAG_TEXT: i32 = 1 << 2;
|
||||
|
||||
const JUMP_BACKWARD_ICON: &[u8] =
|
||||
include_bytes!("../res/icons/hicolor/16x16/apps/jump-backward-10-symbolic.svg");
|
||||
|
||||
const JUMP_FORWARD_ICON: &[u8] =
|
||||
include_bytes!("../res/icons/hicolor/16x16/apps/jump-forward-10-symbolic.svg");
|
||||
|
||||
use std::error::Error;
|
||||
|
||||
fn language_name(code: &str) -> Option<String> {
|
||||
|
|
@ -187,6 +176,8 @@ pub enum Action {
|
|||
FolderOpenRecent(usize),
|
||||
Fullscreen,
|
||||
PlayPause,
|
||||
PlayPrev,
|
||||
PlayNext,
|
||||
SeekBackward,
|
||||
SeekForward,
|
||||
NextFrame,
|
||||
|
|
@ -210,6 +201,8 @@ impl MenuAction for Action {
|
|||
Self::FolderOpenRecent(index) => Message::FolderOpenRecent(*index),
|
||||
Self::Fullscreen => Message::Fullscreen,
|
||||
Self::PlayPause => Message::PlayPause,
|
||||
Self::PlayPrev => Message::PlayPrev,
|
||||
Self::PlayNext => Message::PlayNext,
|
||||
Self::SeekBackward => Message::SeekRelative(-10.0),
|
||||
Self::SeekForward => Message::SeekRelative(10.0),
|
||||
Self::NextFrame => Message::NextFrame,
|
||||
|
|
@ -308,6 +301,7 @@ pub enum Message {
|
|||
Seek(f64),
|
||||
SeekRelative(f64),
|
||||
SeekRelease,
|
||||
PlayPrev,
|
||||
PlayNext,
|
||||
NextFrame,
|
||||
PreviousFrame,
|
||||
|
|
@ -611,10 +605,10 @@ impl App {
|
|||
}
|
||||
|
||||
fn save_config_state(&mut self) {
|
||||
if let Some(ref config_state_handler) = self.flags.config_state_handler {
|
||||
if let Err(err) = self.flags.config_state.write_entry(config_state_handler) {
|
||||
log::error!("failed to save config_state: {}", err);
|
||||
}
|
||||
if let Some(ref config_state_handler) = self.flags.config_state_handler
|
||||
&& let Err(err) = self.flags.config_state.write_entry(config_state_handler)
|
||||
{
|
||||
log::error!("failed to save config_state: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -623,9 +617,9 @@ impl App {
|
|||
|| !self
|
||||
.video_opt
|
||||
.as_ref()
|
||||
.map_or(false, |video| video.has_video())
|
||||
.is_some_and(|video| video.has_video())
|
||||
{
|
||||
self.core.window.show_headerbar = true && !self.fullscreen;
|
||||
self.core.window.show_headerbar = !self.fullscreen;
|
||||
self.controls = true;
|
||||
self.controls_time = Instant::now();
|
||||
} else if self.controls && self.controls_time.elapsed() > CONTROLS_TIMEOUT {
|
||||
|
|
@ -679,7 +673,7 @@ impl App {
|
|||
..Default::default()
|
||||
};
|
||||
//TODO: use any other stream tags?
|
||||
if let Some(tags) = self.audio_tags.get(0) {
|
||||
if let Some(tags) = self.audio_tags.first() {
|
||||
log::info!("{:#?}", tags);
|
||||
if let Some(tag) = tags.get::<gst::tags::Album>() {
|
||||
new.album = tag.get().into();
|
||||
|
|
@ -752,11 +746,11 @@ impl App {
|
|||
new.album_art_opt = url::Url::from_file_path(album_art.path()).ok();
|
||||
}
|
||||
}
|
||||
if let Some((old, _, tx)) = &mut self.mpris_opt {
|
||||
if new != *old {
|
||||
*old = new.clone();
|
||||
let _ = tx.send(MprisEvent::Meta(new.clone()));
|
||||
}
|
||||
if let Some((old, _, tx)) = &mut self.mpris_opt
|
||||
&& new != *old
|
||||
{
|
||||
*old = new.clone();
|
||||
let _ = tx.send(MprisEvent::Meta(new.clone()));
|
||||
}
|
||||
self.mpris_meta = new;
|
||||
}
|
||||
|
|
@ -951,12 +945,17 @@ impl Application for App {
|
|||
|
||||
fn on_escape(&mut self) -> Task<Self::Message> {
|
||||
if self.fullscreen {
|
||||
return self.update(Message::Fullscreen);
|
||||
self.update(Message::Fullscreen)
|
||||
} else {
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
|
||||
fn on_app_exit(&mut self) -> Option<Self::Message> {
|
||||
self.close();
|
||||
None
|
||||
}
|
||||
|
||||
fn on_nav_select(&mut self, id: nav_bar::Id) -> Task<Message> {
|
||||
// Toggle open state and get clone of node data
|
||||
let node_opt = match self.nav_model.data_mut::<ProjectNode>(id) {
|
||||
|
|
@ -1210,12 +1209,12 @@ impl Application for App {
|
|||
}
|
||||
}
|
||||
Message::AudioCode(code) => {
|
||||
if let Ok(code) = i32::try_from(code) {
|
||||
if let Some(video) = &self.video_opt {
|
||||
let pipeline = video.pipeline();
|
||||
pipeline.set_property("current-audio", code);
|
||||
self.current_audio = pipeline.property("current-audio");
|
||||
}
|
||||
if let Ok(code) = i32::try_from(code)
|
||||
&& let Some(video) = &self.video_opt
|
||||
{
|
||||
let pipeline = video.pipeline();
|
||||
pipeline.set_property("current-audio", code);
|
||||
self.current_audio = pipeline.property("current-audio");
|
||||
}
|
||||
}
|
||||
Message::AudioToggle => {
|
||||
|
|
@ -1225,12 +1224,12 @@ impl Application for App {
|
|||
}
|
||||
}
|
||||
Message::AudioVolume(volume) => {
|
||||
if let Some(video) = &mut self.video_opt {
|
||||
if volume >= 0.0 && volume <= 1.0 {
|
||||
video.set_volume(volume);
|
||||
video.set_muted(false);
|
||||
self.update_controls(true);
|
||||
}
|
||||
if let Some(video) = &mut self.video_opt
|
||||
&& (0.0..=1.0).contains(&volume)
|
||||
{
|
||||
video.set_volume(volume);
|
||||
video.set_muted(false);
|
||||
self.update_controls(true);
|
||||
}
|
||||
}
|
||||
Message::TextCode(index) => {
|
||||
|
|
@ -1313,7 +1312,7 @@ impl Application for App {
|
|||
}
|
||||
}
|
||||
|
||||
if (volume >= 0.0 && volume <= 1.0) && !nav_bar_toggled {
|
||||
if (0.0..=1.0).contains(&volume) && !nav_bar_toggled {
|
||||
video.set_volume(volume);
|
||||
video.set_muted(false);
|
||||
self.update_controls(true);
|
||||
|
|
@ -1355,6 +1354,55 @@ impl Application for App {
|
|||
}
|
||||
}
|
||||
|
||||
Message::PlayPrev => {
|
||||
if self.flags.config_state.player_state.repeat == RepeatState::Track {
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
//first we get info about current media id & position in nav_bar
|
||||
let curr_id = self.nav_model.active();
|
||||
let curr_position = match self.nav_model.position(curr_id) {
|
||||
Some(pos) => pos,
|
||||
None => {
|
||||
log::warn!("Failed to get position of current media: {:?}", curr_id);
|
||||
return self.update(Message::EndOfStream);
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(video) = &mut self.video_opt {
|
||||
self.position = video.position().as_secs_f64();
|
||||
|
||||
if self.position > 3.0 {
|
||||
video.seek(0, false).expect("seek");
|
||||
} else {
|
||||
if self.nav_model.activate_position(curr_position - 1) {
|
||||
let curr_id = self.nav_model.active();
|
||||
match self.nav_model.data::<ProjectNode>(curr_id) {
|
||||
//The prev one is a media file, we play it.
|
||||
Some(ProjectNode::File { .. }) => {
|
||||
return self.on_nav_select(curr_id);
|
||||
}
|
||||
|
||||
//The prev one is a folder. We expand it and recall PlayPrev.
|
||||
Some(ProjectNode::Folder { .. }) => {
|
||||
let _ = self.on_nav_select(curr_id);
|
||||
return self.update(Message::PlayPrev);
|
||||
}
|
||||
|
||||
//Unknown type. We do nothing.
|
||||
_ => log::warn!(
|
||||
"unknown type: {:?}",
|
||||
self.nav_model.data::<ProjectNode>(curr_id)
|
||||
),
|
||||
}
|
||||
} else {
|
||||
// first file
|
||||
video.seek(0, false).expect("seek");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Message::PlayNext => {
|
||||
// TODO: known limitations:
|
||||
// 1) if the user collapses the folder entry while a song is playing,
|
||||
|
|
@ -1537,20 +1585,20 @@ impl Application for App {
|
|||
self.update_mpris_state();
|
||||
}
|
||||
Message::NewFrame => {
|
||||
if let Some(video) = &mut self.video_opt {
|
||||
if !self.dragging {
|
||||
self.position = video.position().as_secs_f64();
|
||||
if let Some(video) = &mut self.video_opt
|
||||
&& !self.dragging
|
||||
{
|
||||
self.position = video.position().as_secs_f64();
|
||||
|
||||
if let Some((a, b)) = self.ab_repeat {
|
||||
let target_a = a.unwrap_or(0.0);
|
||||
let target_b = b.unwrap_or(self.duration);
|
||||
if self.position >= target_b {
|
||||
let _ = video.seek(Duration::from_secs_f64(target_a), true);
|
||||
}
|
||||
if let Some((a, b)) = self.ab_repeat {
|
||||
let target_a = a.unwrap_or(0.0);
|
||||
let target_b = b.unwrap_or(self.duration);
|
||||
if self.position >= target_b {
|
||||
let _ = video.seek(Duration::from_secs_f64(target_a), true);
|
||||
}
|
||||
|
||||
self.update_controls(self.dropdown_opt.is_some());
|
||||
}
|
||||
|
||||
self.update_controls(self.dropdown_opt.is_some());
|
||||
}
|
||||
}
|
||||
Message::Reload => {
|
||||
|
|
@ -1563,7 +1611,8 @@ impl Application for App {
|
|||
return self.update_config();
|
||||
}
|
||||
Message::WindowClose => {
|
||||
process::exit(0);
|
||||
self.close();
|
||||
return exit();
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
|
|
@ -1796,19 +1845,55 @@ impl Application for App {
|
|||
);
|
||||
}
|
||||
if self.controls {
|
||||
let mut row = widget::row::with_capacity(7)
|
||||
let mut row = widget::row::with_capacity(9)
|
||||
.align_y(Alignment::Center)
|
||||
.spacing(space_xxs)
|
||||
.push(
|
||||
widget::button::icon(
|
||||
if self.video_opt.as_ref().map_or(true, |video| video.paused()) {
|
||||
widget::icon::from_name("media-playback-start-symbolic").size(16)
|
||||
} else {
|
||||
widget::icon::from_name("media-playback-pause-symbolic").size(16)
|
||||
},
|
||||
)
|
||||
.on_press(Message::PlayPause),
|
||||
if self
|
||||
.video_opt
|
||||
.as_ref()
|
||||
.map_or(true, |video| video.has_video())
|
||||
{
|
||||
widget::button::icon(
|
||||
widget::icon::from_svg_bytes(JUMP_BACKWARD_ICON).symbolic(true),
|
||||
)
|
||||
.on_press(Message::SeekRelative(-10.0))
|
||||
} else {
|
||||
widget::button::icon(
|
||||
widget::icon::from_name("media-skip-backward-symbolic").size(16),
|
||||
)
|
||||
.on_press(Message::PlayPrev)
|
||||
},
|
||||
);
|
||||
row = row.push(
|
||||
widget::button::icon(
|
||||
if self.video_opt.as_ref().map_or(true, |video| video.paused()) {
|
||||
widget::icon::from_name("media-playback-start-symbolic").size(16)
|
||||
} else {
|
||||
widget::icon::from_name("media-playback-pause-symbolic").size(16)
|
||||
},
|
||||
)
|
||||
.on_press(Message::PlayPause),
|
||||
);
|
||||
|
||||
row = row.push(
|
||||
if self
|
||||
.video_opt
|
||||
.as_ref()
|
||||
.map_or(true, |video| video.has_video())
|
||||
{
|
||||
widget::button::icon(
|
||||
widget::icon::from_svg_bytes(JUMP_FORWARD_ICON).symbolic(true),
|
||||
)
|
||||
.on_press(Message::SeekRelative(10.0))
|
||||
} else {
|
||||
widget::button::icon(
|
||||
widget::icon::from_name("media-skip-forward-symbolic").size(16),
|
||||
)
|
||||
.on_press(Message::PlayNext)
|
||||
},
|
||||
);
|
||||
|
||||
row = row.push(widget::tooltip(
|
||||
widget::button::icon(
|
||||
widget::icon::from_name(match self.flags.config_state.player_state.repeat {
|
||||
|
|
|
|||
23
src/menu.rs
23
src/menu.rs
|
|
@ -1,14 +1,11 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::{
|
||||
theme,
|
||||
widget::{
|
||||
menu::{self, key_bind::KeyBind, ItemHeight, ItemWidth, MenuBar},
|
||||
RcElementWrapper,
|
||||
},
|
||||
Element,
|
||||
};
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use cosmic::widget::RcElementWrapper;
|
||||
use cosmic::widget::menu::key_bind::KeyBind;
|
||||
use cosmic::widget::menu::{self, ItemHeight, ItemWidth, MenuBar};
|
||||
use cosmic::{Element, theme};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{Action, Config, ConfigState, Message, fl};
|
||||
|
||||
|
|
@ -22,10 +19,10 @@ pub fn menu_bar<'a>(
|
|||
) -> Element<'a, Message> {
|
||||
let home_dir_opt = std::env::home_dir();
|
||||
let format_path = |path: &PathBuf| -> String {
|
||||
if let Some(home_dir) = &home_dir_opt {
|
||||
if let Ok(part) = path.strip_prefix(home_dir) {
|
||||
return format!("~/{}", part.display());
|
||||
}
|
||||
if let Some(home_dir) = &home_dir_opt
|
||||
&& let Ok(part) = path.strip_prefix(home_dir)
|
||||
{
|
||||
return format!("~/{}", part.display());
|
||||
}
|
||||
path.display().to_string()
|
||||
};
|
||||
|
|
|
|||
24
src/mpris.rs
24
src/mpris.rs
|
|
@ -1,18 +1,16 @@
|
|||
use cosmic::{
|
||||
iced::{
|
||||
futures::{self, SinkExt, Stream},
|
||||
Subscription, stream,
|
||||
},
|
||||
};
|
||||
use cosmic::iced::futures::{self, SinkExt, Stream};
|
||||
use cosmic::iced::{Subscription, stream};
|
||||
use mpris_server::zbus::{Result, fdo};
|
||||
use mpris_server::{
|
||||
LoopStatus, Metadata, PlaybackRate, PlaybackStatus, PlayerInterface, Property, RootInterface,
|
||||
Server, Signal, Time, TrackId, Volume,
|
||||
zbus::{Result, fdo},
|
||||
};
|
||||
use std::{any::TypeId, future, process};
|
||||
use std::any::TypeId;
|
||||
use std::{future, process};
|
||||
use tokio::sync::{Mutex, mpsc};
|
||||
|
||||
use crate::{Message, MprisEvent, MprisMeta, MprisState, config::RepeatState};
|
||||
use crate::config::RepeatState;
|
||||
use crate::{Message, MprisEvent, MprisMeta, MprisState};
|
||||
|
||||
impl MprisMeta {
|
||||
fn metadata(&self) -> Metadata {
|
||||
|
|
@ -152,12 +150,12 @@ impl RootInterface for Player {
|
|||
impl PlayerInterface for Player {
|
||||
async fn next(&self) -> fdo::Result<()> {
|
||||
log::info!("Next");
|
||||
Ok(())
|
||||
self.message(Message::PlayNext).await
|
||||
}
|
||||
|
||||
async fn previous(&self) -> fdo::Result<()> {
|
||||
log::info!("Previous");
|
||||
Ok(())
|
||||
self.message(Message::PlayPrev).await
|
||||
}
|
||||
|
||||
async fn pause(&self) -> fdo::Result<()> {
|
||||
|
|
@ -273,12 +271,12 @@ impl PlayerInterface for Player {
|
|||
|
||||
async fn can_go_next(&self) -> fdo::Result<bool> {
|
||||
log::info!("CanGoNext");
|
||||
Ok(false)
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn can_go_previous(&self) -> fdo::Result<bool> {
|
||||
log::info!("CanGoPrevious");
|
||||
Ok(false)
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
async fn can_play(&self) -> fdo::Result<bool> {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::widget;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
fs, io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use std::cmp::Ordering;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{fs, io};
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ProjectNode {
|
||||
|
|
@ -26,15 +24,15 @@ impl ProjectNode {
|
|||
let path = fs::canonicalize(path)?;
|
||||
let name = path
|
||||
.file_name()
|
||||
.ok_or(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("path {:?} has no file name", path),
|
||||
))?
|
||||
.ok_or(io::Error::other(format!(
|
||||
"path {:?} has no file name",
|
||||
path
|
||||
)))?
|
||||
.to_str()
|
||||
.ok_or(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("path {:?} is not valid UTF-8", path),
|
||||
))?
|
||||
.ok_or(io::Error::other(format!(
|
||||
"path {:?} is not valid UTF-8",
|
||||
path
|
||||
)))?
|
||||
.to_string();
|
||||
Ok(if path.is_dir() {
|
||||
Self::Folder {
|
||||
|
|
@ -94,7 +92,7 @@ impl Ord for ProjectNode {
|
|||
}
|
||||
}
|
||||
}
|
||||
crate::localize::sorter().compare(self.name(), other.name())
|
||||
crate::localize::LANGUAGE_SORTER.compare(self.name(), other.name())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
use iced_video_player::Position;
|
||||
use image::{DynamicImage, ImageFormat, RgbaImage};
|
||||
use std::{error::Error, num::NonZero, path::Path, time::Duration};
|
||||
use std::error::Error;
|
||||
use std::num::NonZero;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use url::Url;
|
||||
|
||||
use super::video;
|
||||
|
|
@ -14,7 +17,7 @@ pub fn main(
|
|||
let thumbnails = {
|
||||
let mut video = match video::new_video(input, video::VideoSettings { mute: true }) {
|
||||
Ok(ok) => ok,
|
||||
Err(_err) => return Err(Into::into(format!("missing required plugin"))),
|
||||
Err(_err) => return Err(Into::into("missing required plugin".to_string())),
|
||||
};
|
||||
|
||||
let duration = video.duration();
|
||||
|
|
@ -37,7 +40,7 @@ pub fn main(
|
|||
pixels,
|
||||
} => RgbaImage::from_raw(*width, *height, pixels.to_vec())
|
||||
.map(DynamicImage::ImageRgba8)
|
||||
.ok_or_else(|| format!("failed to convert thumbnail")),
|
||||
.ok_or_else(|| "failed to convert thumbnail".to_string()),
|
||||
_ => Err(format!("unsupported thumbnail handle {:?}", thumbnails[0])),
|
||||
}
|
||||
}?;
|
||||
|
|
|
|||
45
src/video.rs
45
src/video.rs
|
|
@ -1,10 +1,8 @@
|
|||
use iced_video_player::{
|
||||
Video,
|
||||
gst::{self, prelude::*},
|
||||
gst_app, gst_pbutils,
|
||||
};
|
||||
use iced_video_player::gst::prelude::*;
|
||||
use iced_video_player::{Video, gst, gst_app, gst_pbutils};
|
||||
|
||||
use cosmic::{action, app::Task};
|
||||
use cosmic::action;
|
||||
use cosmic::app::Task;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct VideoSettings {
|
||||
|
|
@ -33,13 +31,13 @@ pub fn new_video(
|
|||
let Ok(elem) = vals[1].get::<gst::Element>() else {
|
||||
return None;
|
||||
};
|
||||
if let Some(factory) = elem.factory() {
|
||||
if factory.name() == "souphttpsrc" {
|
||||
elem.set_property(
|
||||
"user-agent",
|
||||
"Mozilla/5.0 (X11; Linux x86_64; rv:142.0) Gecko/20100101 Firefox/142.0",
|
||||
);
|
||||
}
|
||||
if let Some(factory) = elem.factory()
|
||||
&& factory.name() == "souphttpsrc"
|
||||
{
|
||||
elem.set_property(
|
||||
"user-agent",
|
||||
"Mozilla/5.0 (X11; Linux x86_64; rv:142.0) Gecko/20100101 Firefox/142.0",
|
||||
);
|
||||
}
|
||||
None
|
||||
});
|
||||
|
|
@ -65,18 +63,15 @@ pub fn new_video(
|
|||
.unwrap()
|
||||
.pop_filtered(&[gst::MessageType::Element])
|
||||
{
|
||||
match msg.view() {
|
||||
gst::MessageView::Element(element) => {
|
||||
if gst_pbutils::MissingPluginMessage::is(&element) {
|
||||
commands.push(Task::perform(
|
||||
async { action::app(super::Message::MissingPlugin(msg)) },
|
||||
|x| x,
|
||||
));
|
||||
// Do one codec install at a time
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
if let gst::MessageView::Element(element) = msg.view()
|
||||
&& gst_pbutils::MissingPluginMessage::is(element)
|
||||
{
|
||||
commands.push(Task::perform(
|
||||
async { action::app(super::Message::MissingPlugin(msg)) },
|
||||
|x| x,
|
||||
));
|
||||
// Do one codec install at a time
|
||||
break;
|
||||
}
|
||||
}
|
||||
pipeline.set_state(gst::State::Null).unwrap();
|
||||
|
|
|
|||
|
|
@ -3,10 +3,8 @@
|
|||
|
||||
//! Integrations with XDG portals.
|
||||
|
||||
use ashpd::{
|
||||
desktop::inhibit::{InhibitFlags, InhibitProxy},
|
||||
enumflags2::{BitFlags, make_bitflags},
|
||||
};
|
||||
use ashpd::desktop::inhibit::{InhibitFlags, InhibitProxy};
|
||||
use ashpd::enumflags2::{BitFlags, make_bitflags};
|
||||
use log::{debug, warn};
|
||||
use tokio::sync::watch::Receiver;
|
||||
|
||||
|
|
|
|||
2
vendor/iced_video_player/.gitignore
vendored
Normal file
2
vendor/iced_video_player/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
.direnv
|
||||
88
vendor/iced_video_player/Cargo.toml
vendored
Normal file
88
vendor/iced_video_player/Cargo.toml
vendored
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
[package]
|
||||
name = "iced_video_player"
|
||||
description = "A convenient video player widget for Iced"
|
||||
homepage = "https://github.com/jazzfool/iced_video_player"
|
||||
repository = "https://github.com/jazzfool/iced_video_player"
|
||||
readme = "README.md"
|
||||
keywords = ["gui", "iced", "video"]
|
||||
categories = ["gui", "multimedia"]
|
||||
version = "0.6.0"
|
||||
authors = ["jazzfool"]
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
license = "MIT OR Apache-2.0"
|
||||
exclude = [".media/test.mp4"]
|
||||
|
||||
[dependencies]
|
||||
gstreamer = "0.25"
|
||||
gstreamer-app = "0.25" # appsink
|
||||
gstreamer-base = "0.25" # basesrc
|
||||
gstreamer-pbutils = "0.25"
|
||||
glib = "0.22" # gobject traits and error type
|
||||
log = "0.4"
|
||||
thiserror = "2"
|
||||
url = "2" # media uri
|
||||
html-escape = "0.2.13"
|
||||
iced_wgpu = { path = "../../../libcosmic/iced/wgpu", default-features = false, optional = true }
|
||||
|
||||
[dependencies.cosmic]
|
||||
package = "libcosmic-yoda"
|
||||
path = "../../../libcosmic"
|
||||
default-features = false
|
||||
features = ["tokio", "winit"]
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.11"
|
||||
|
||||
[features]
|
||||
default = ["wgpu"]
|
||||
wgpu = ["cosmic/wgpu", "iced_wgpu"]
|
||||
|
||||
[package.metadata.nix]
|
||||
systems = ["x86_64-linux"]
|
||||
app = true
|
||||
build = true
|
||||
runtimeLibs = [
|
||||
"vulkan-loader",
|
||||
"wayland",
|
||||
"wayland-protocols",
|
||||
"libxkbcommon",
|
||||
"xorg.libX11",
|
||||
"xorg.libXrandr",
|
||||
"xorg.libXi",
|
||||
"gst_all_1.gstreamer",
|
||||
"gst_all_1.gstreamermm",
|
||||
"gst_all_1.gst-plugins-bad",
|
||||
"gst_all_1.gst-plugins-ugly",
|
||||
"gst_all_1.gst-plugins-good",
|
||||
"gst_all_1.gst-plugins-base",
|
||||
]
|
||||
buildInputs = [
|
||||
"libxkbcommon",
|
||||
"gst_all_1.gstreamer",
|
||||
"gst_all_1.gstreamermm",
|
||||
"gst_all_1.gst-plugins-bad",
|
||||
"gst_all_1.gst-plugins-ugly",
|
||||
"gst_all_1.gst-plugins-good",
|
||||
"gst_all_1.gst-plugins-base",
|
||||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
rustc-args = ["--cfg", "docsrs"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
|
||||
|
||||
# [patch.'https://github.com/pop-os/libcosmic']
|
||||
# libcosmic = { path = "../libcosmic" }
|
||||
# cosmic-config = { path = "../libcosmic/cosmic-config" }
|
||||
# cosmic-theme = { path = "../libcosmic/cosmic-theme" }
|
||||
# libcosmic = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
|
||||
# cosmic-config = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
|
||||
# cosmic-theme = { git = "https://github.com/pop-os/libcosmic//", branch = "iced-rebase" }
|
||||
|
||||
[dev_dependencies]
|
||||
libcosmic = { git = "https://github.com/pop-os/libcosmic", features = [
|
||||
"wgpu",
|
||||
"wayland",
|
||||
] }
|
||||
176
vendor/iced_video_player/LICENSE-APACHE
vendored
Normal file
176
vendor/iced_video_player/LICENSE-APACHE
vendored
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
23
vendor/iced_video_player/LICENSE-MIT
vendored
Normal file
23
vendor/iced_video_player/LICENSE-MIT
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
64
vendor/iced_video_player/README.md
vendored
Normal file
64
vendor/iced_video_player/README.md
vendored
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Iced Video Player Widget
|
||||
|
||||
Composable component to play videos in any Iced application built on the excellent GStreamer library.
|
||||
|
||||
<img src=".media/screenshot.png" width="50%" />
|
||||
|
||||
## Overview
|
||||
|
||||
In general, this supports anything that [`gstreamer/playbin`](https://gstreamer.freedesktop.org/documentation/playback/playbin.html?gi-language=c) supports.
|
||||
|
||||
Features:
|
||||
- Load video files from any file path **or URL** (support for streaming over network).
|
||||
- Video buffering when streaming on a network.
|
||||
- Audio support.
|
||||
- Programmatic control.
|
||||
- Can capture thumbnails from a set of timestamps.
|
||||
- Good performance (i.e., comparable to other video players). GStreamer (with the right plugins) will perform hardware-accelerated decoding, and the color space (YUV to RGB) is converted on the GPU whilst rendering the frame.
|
||||
|
||||
Limitations (hopefully to be fixed):
|
||||
- GStreamer is a bit annoying to set up on Windows.
|
||||
|
||||
The player **does not** come with any surrounding GUI controls, but they should be quite easy to implement should you need them.
|
||||
See the "minimal" example for a demonstration on how you could implement pausing, looping, and seeking.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```rust
|
||||
use iced_video_player::{Video, VideoPlayer};
|
||||
|
||||
fn main() -> iced::Result {
|
||||
iced::run("Video Player", (), App::view)
|
||||
}
|
||||
|
||||
struct App {
|
||||
video: Video,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> Self {
|
||||
App {
|
||||
video: Video::new(&url::Url::parse("file:///C:/my_video.mp4").unwrap()).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn view(&self) -> iced::Element<()> {
|
||||
VideoPlayer::new(&self.video).into()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
Follow the [GStreamer build instructions](https://github.com/sdroege/gstreamer-rs#installation). This should be able to compile on MSVC, MinGW, Linux, and MacOS.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either
|
||||
|
||||
- [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
- [MIT](http://opensource.org/licenses/MIT)
|
||||
|
||||
at your option.
|
||||
154
vendor/iced_video_player/examples/minimal.rs
vendored
Normal file
154
vendor/iced_video_player/examples/minimal.rs
vendored
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use cosmic::iced::{self, Program, Task, Theme};
|
||||
|
||||
use iced::{
|
||||
application::Application,
|
||||
widget::{Button, Column, Container, Row, Slider, Text},
|
||||
};
|
||||
use iced_video_player::{Video, VideoPlayer};
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
application().run()
|
||||
}
|
||||
|
||||
fn application() -> Application<impl Program<Message = Message, Theme = Theme>> {
|
||||
iced::application(App::default, App::update, App::view).window_size((500.0, 800.0))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Message {
|
||||
TogglePause,
|
||||
ToggleLoop,
|
||||
Seek(f64),
|
||||
SeekRelease,
|
||||
EndOfStream,
|
||||
NewFrame,
|
||||
}
|
||||
|
||||
struct App {
|
||||
video: Video,
|
||||
position: f64,
|
||||
dragging: bool,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
fn default() -> Self {
|
||||
App {
|
||||
video: Video::new(
|
||||
&url::Url::from_file_path(
|
||||
std::path::PathBuf::from(file!())
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("../.media/test.mp4")
|
||||
.canonicalize()
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
position: 0.0,
|
||||
dragging: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::TogglePause => {
|
||||
self.video.set_paused(!self.video.paused());
|
||||
}
|
||||
Message::ToggleLoop => {
|
||||
self.video.set_looping(!self.video.looping());
|
||||
}
|
||||
Message::Seek(secs) => {
|
||||
self.dragging = true;
|
||||
self.video.set_paused(true);
|
||||
self.position = secs;
|
||||
}
|
||||
Message::SeekRelease => {
|
||||
self.dragging = false;
|
||||
self.video
|
||||
.seek(Duration::from_secs_f64(self.position), false)
|
||||
.expect("seek");
|
||||
self.video.set_paused(false);
|
||||
}
|
||||
Message::EndOfStream => {
|
||||
println!("end of stream");
|
||||
}
|
||||
Message::NewFrame => {
|
||||
if !self.dragging {
|
||||
self.position = self.video.position().as_secs_f64();
|
||||
}
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn view(&self) -> iced::Element<'_, Message> {
|
||||
Column::new()
|
||||
.push(
|
||||
Container::new(
|
||||
VideoPlayer::new(&self.video)
|
||||
.width(iced::Length::Fill)
|
||||
.height(iced::Length::Fill)
|
||||
.content_fit(iced::ContentFit::Contain)
|
||||
.on_end_of_stream(Message::EndOfStream)
|
||||
.on_new_frame(Message::NewFrame),
|
||||
)
|
||||
.align_x(iced::Alignment::Center)
|
||||
.align_y(iced::Alignment::Center)
|
||||
.width(iced::Length::Fill)
|
||||
.height(iced::Length::Fill),
|
||||
)
|
||||
.push(
|
||||
Container::new(
|
||||
Slider::new(
|
||||
0.0..=self.video.duration().as_secs_f64(),
|
||||
self.position,
|
||||
Message::Seek,
|
||||
)
|
||||
.step(0.1)
|
||||
.on_release(Message::SeekRelease),
|
||||
)
|
||||
.padding(iced::Padding::new(5.0).left(10.0).right(10.0)),
|
||||
)
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(5)
|
||||
.align_y(iced::alignment::Vertical::Center)
|
||||
.padding(iced::Padding::new(10.0).top(0.0))
|
||||
.push(
|
||||
Button::new(Text::new(if self.video.paused() {
|
||||
"Play"
|
||||
} else {
|
||||
"Pause"
|
||||
}))
|
||||
.width(80.0)
|
||||
.on_press(Message::TogglePause),
|
||||
)
|
||||
.push(
|
||||
Button::new(Text::new(if self.video.looping() {
|
||||
"Disable Loop"
|
||||
} else {
|
||||
"Enable Loop"
|
||||
}))
|
||||
.width(120.0)
|
||||
.on_press(Message::ToggleLoop),
|
||||
)
|
||||
.push(
|
||||
Text::new(format!(
|
||||
"{}:{:02}s / {}:{:02}s",
|
||||
self.position as u64 / 60,
|
||||
self.position as u64 % 60,
|
||||
self.video.duration().as_secs() / 60,
|
||||
self.video.duration().as_secs() % 60,
|
||||
))
|
||||
.width(iced::Length::Fill)
|
||||
.align_x(iced::alignment::Horizontal::Right),
|
||||
),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
79
vendor/iced_video_player/src/lib.rs
vendored
Normal file
79
vendor/iced_video_player/src/lib.rs
vendored
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
//! # Iced Video Player
|
||||
//!
|
||||
//! A convenient video player widget for Iced.
|
||||
//!
|
||||
//! To get started, load a video from a URI (e.g., a file path prefixed with `file:///`) using [`Video::new`](crate::Video::new),
|
||||
//! then use it like any other Iced widget in your `view` function by creating a [`VideoPlayer`].
|
||||
//!
|
||||
//! Example:
|
||||
//! ```rust
|
||||
//! use iced_video_player::{Video, VideoPlayer};
|
||||
//!
|
||||
//! fn main() -> iced::Result {
|
||||
//! iced::run("Video Player", (), App::view)
|
||||
//! }
|
||||
//!
|
||||
//! struct App {
|
||||
//! video: Video,
|
||||
//! }
|
||||
//!
|
||||
//! impl Default for App {
|
||||
//! fn default() -> Self {
|
||||
//! App {
|
||||
//! video: Video::new(&url::Url::parse("file:///C:/my_video.mp4").unwrap()).unwrap(),
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! impl App {
|
||||
//! fn view(&self) -> iced::Element<()> {
|
||||
//! VideoPlayer::new(&self.video).into()
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! You can programmatically control the video (e.g., seek, pause, loop, grab thumbnails) by accessing various methods on [`Video`].
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
mod pipeline;
|
||||
mod video;
|
||||
mod video_player;
|
||||
|
||||
pub use gstreamer as gst;
|
||||
pub use gstreamer_app as gst_app;
|
||||
pub use gstreamer_pbutils as gst_pbutils;
|
||||
use thiserror::Error;
|
||||
|
||||
pub use video::Position;
|
||||
pub use video::Video;
|
||||
pub use video_player::VideoPlayer;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("{0}")]
|
||||
Glib(#[from] glib::Error),
|
||||
#[error("{0}")]
|
||||
Bool(#[from] glib::BoolError),
|
||||
#[error("failed to get the gstreamer bus")]
|
||||
Bus,
|
||||
#[error("failed to get AppSink element with name='{0}' from gstreamer pipeline")]
|
||||
AppSink(String),
|
||||
#[error("{0}")]
|
||||
StateChange(#[from] gst::StateChangeError),
|
||||
#[error("failed to cast gstreamer element")]
|
||||
Cast,
|
||||
#[error("{0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("invalid URI")]
|
||||
Uri,
|
||||
#[error("failed to get media capabilities")]
|
||||
Caps,
|
||||
#[error("failed to query media duration or position")]
|
||||
Duration,
|
||||
#[error("failed to sync with playback")]
|
||||
Sync,
|
||||
#[error("failed to lock internal sync primitive")]
|
||||
Lock,
|
||||
#[error("invalid framerate: {0}")]
|
||||
Framerate(f64),
|
||||
}
|
||||
447
vendor/iced_video_player/src/pipeline.rs
vendored
Normal file
447
vendor/iced_video_player/src/pipeline.rs
vendored
Normal file
|
|
@ -0,0 +1,447 @@
|
|||
use cosmic::iced::wgpu;
|
||||
use cosmic::iced::widget::shader::{Pipeline, Primitive, Viewport};
|
||||
use cosmic::iced::{self, Rectangle};
|
||||
use std::{
|
||||
collections::{btree_map::Entry, BTreeMap},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::video::Frame;
|
||||
|
||||
#[repr(C)]
|
||||
struct Uniforms {
|
||||
rect: [f32; 4],
|
||||
}
|
||||
|
||||
struct VideoEntry {
|
||||
texture_y: wgpu::Texture,
|
||||
texture_uv: wgpu::Texture,
|
||||
uniforms: wgpu::Buffer,
|
||||
bg0: wgpu::BindGroup,
|
||||
alive: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
pub struct VideoPipeline {
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
bg0_layout: wgpu::BindGroupLayout,
|
||||
sampler: wgpu::Sampler,
|
||||
videos: BTreeMap<u64, VideoEntry>,
|
||||
}
|
||||
|
||||
impl VideoPipeline {
|
||||
fn new(device: &wgpu::Device, format: wgpu::TextureFormat, queue: &wgpu::Queue) -> Self {
|
||||
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("iced_video_player shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()),
|
||||
});
|
||||
|
||||
let bg0_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("iced_video_player bind group 0 layout"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 2,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count: None,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 3,
|
||||
visibility: wgpu::ShaderStages::VERTEX,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("iced_video_player pipeline layout"),
|
||||
bind_group_layouts: &[&bg0_layout],
|
||||
push_constant_ranges: &[],
|
||||
});
|
||||
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("iced_video_player pipeline"),
|
||||
layout: Some(&layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &shader,
|
||||
entry_point: Some("vs_main"),
|
||||
buffers: &[],
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
},
|
||||
primitive: wgpu::PrimitiveState::default(),
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState {
|
||||
count: 1,
|
||||
mask: !0,
|
||||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &shader,
|
||||
entry_point: Some("fs_main"),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend: None,
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
}),
|
||||
multiview: None,
|
||||
cache: None,
|
||||
});
|
||||
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("iced_video_player sampler"),
|
||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||
mag_filter: wgpu::FilterMode::Linear,
|
||||
min_filter: wgpu::FilterMode::Linear,
|
||||
mipmap_filter: wgpu::FilterMode::Nearest,
|
||||
lod_min_clamp: 0.0,
|
||||
lod_max_clamp: 1.0,
|
||||
compare: None,
|
||||
anisotropy_clamp: 1,
|
||||
border_color: None,
|
||||
});
|
||||
|
||||
VideoPipeline {
|
||||
pipeline,
|
||||
bg0_layout,
|
||||
sampler,
|
||||
videos: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn upload(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
video_id: u64,
|
||||
alive: &Arc<AtomicBool>,
|
||||
(width, height): (u32, u32),
|
||||
frame: &[u8],
|
||||
) {
|
||||
if width == 0 || height == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Entry::Vacant(entry) = self.videos.entry(video_id) {
|
||||
let texture_y = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("iced_video_player texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::R8Unorm,
|
||||
usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
let texture_uv = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("iced_video_player texture"),
|
||||
size: wgpu::Extent3d {
|
||||
width: width / 2,
|
||||
height: height / 2,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rg8Unorm,
|
||||
usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
let view_y = texture_y.create_view(&wgpu::TextureViewDescriptor {
|
||||
label: Some("iced_video_player texture view"),
|
||||
format: None,
|
||||
dimension: None,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
base_mip_level: 0,
|
||||
mip_level_count: None,
|
||||
base_array_layer: 0,
|
||||
array_layer_count: None,
|
||||
usage: None,
|
||||
});
|
||||
|
||||
let view_uv = texture_uv.create_view(&wgpu::TextureViewDescriptor {
|
||||
label: Some("iced_video_player texture view"),
|
||||
format: None,
|
||||
dimension: None,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
base_mip_level: 0,
|
||||
mip_level_count: None,
|
||||
base_array_layer: 0,
|
||||
array_layer_count: None,
|
||||
usage: None,
|
||||
});
|
||||
|
||||
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("iced_video_player uniform buffer"),
|
||||
size: std::mem::size_of::<Uniforms>() as _,
|
||||
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::UNIFORM,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
|
||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("iced_video_player bind group"),
|
||||
layout: &self.bg0_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&view_y),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::TextureView(&view_uv),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: wgpu::BindingResource::Sampler(&self.sampler),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||||
buffer: &buffer,
|
||||
offset: 0,
|
||||
size: None,
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
entry.insert(VideoEntry {
|
||||
texture_y,
|
||||
texture_uv,
|
||||
uniforms: buffer,
|
||||
bg0: bind_group,
|
||||
alive: Arc::clone(alive),
|
||||
});
|
||||
}
|
||||
|
||||
let VideoEntry {
|
||||
texture_y,
|
||||
texture_uv,
|
||||
..
|
||||
} = self.videos.get(&video_id).unwrap();
|
||||
|
||||
queue.write_texture(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: texture_y,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
&frame[..(width * height) as usize],
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(width),
|
||||
rows_per_image: Some(height),
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
|
||||
queue.write_texture(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: texture_uv,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
&frame[(width * height) as usize..],
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(width),
|
||||
rows_per_image: Some(height / 2),
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width: width / 2,
|
||||
height: height / 2,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn cleanup(&mut self) {
|
||||
let ids: Vec<_> = self
|
||||
.videos
|
||||
.iter()
|
||||
.filter_map(|(id, entry)| (!entry.alive.load(Ordering::SeqCst)).then_some(*id))
|
||||
.collect();
|
||||
for id in ids {
|
||||
if let Some(video) = self.videos.remove(&id) {
|
||||
video.texture_y.destroy();
|
||||
video.texture_uv.destroy();
|
||||
video.uniforms.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare(&mut self, queue: &wgpu::Queue, video_id: u64, bounds: &iced::Rectangle) {
|
||||
if let Some(video) = self.videos.get(&video_id) {
|
||||
let uniforms = Uniforms {
|
||||
rect: [
|
||||
bounds.x,
|
||||
bounds.y,
|
||||
bounds.x + bounds.width,
|
||||
bounds.y + bounds.height,
|
||||
],
|
||||
};
|
||||
queue.write_buffer(&video.uniforms, 0, unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
&uniforms as *const _ as *const u8,
|
||||
std::mem::size_of::<Uniforms>(),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
self.cleanup();
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
target: &wgpu::TextureView,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
viewport: &iced::Rectangle<u32>,
|
||||
video_id: u64,
|
||||
) {
|
||||
if let Some(video) = self.videos.get(&video_id) {
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("iced_video_player render pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: target,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
depth_slice: None,
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_bind_group(0, &video.bg0, &[]);
|
||||
pass.set_viewport(
|
||||
viewport.x as _,
|
||||
viewport.y as _,
|
||||
viewport.width as _,
|
||||
viewport.height as _,
|
||||
0.0,
|
||||
1.0,
|
||||
);
|
||||
pass.draw(0..4, 0..1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pipeline for VideoPipeline {
|
||||
fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
VideoPipeline::new(device, format, queue)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct VideoPrimitive {
|
||||
video_id: u64,
|
||||
alive: Arc<AtomicBool>,
|
||||
frame: Arc<Mutex<Frame>>,
|
||||
size: (u32, u32),
|
||||
upload_frame: bool,
|
||||
}
|
||||
|
||||
impl VideoPrimitive {
|
||||
pub fn new(
|
||||
video_id: u64,
|
||||
alive: Arc<AtomicBool>,
|
||||
frame: Arc<Mutex<Frame>>,
|
||||
size: (u32, u32),
|
||||
upload_frame: bool,
|
||||
) -> Self {
|
||||
VideoPrimitive {
|
||||
video_id,
|
||||
alive,
|
||||
frame,
|
||||
size,
|
||||
upload_frame,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Primitive for VideoPrimitive {
|
||||
type Pipeline = VideoPipeline;
|
||||
|
||||
fn prepare(
|
||||
&self,
|
||||
pipeline: &mut Self::Pipeline,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
bounds: &Rectangle,
|
||||
viewport: &Viewport,
|
||||
) {
|
||||
if self.upload_frame {
|
||||
if let Some(readable) = self.frame.lock().expect("lock frame mutex").readable() {
|
||||
pipeline.upload(
|
||||
device,
|
||||
queue,
|
||||
self.video_id,
|
||||
&self.alive,
|
||||
self.size,
|
||||
readable.as_slice(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pipeline.prepare(queue, self.video_id, &bounds);
|
||||
}
|
||||
|
||||
fn render(
|
||||
&self,
|
||||
pipeline: &Self::Pipeline,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
target: &wgpu::TextureView,
|
||||
clip_bounds: &Rectangle<u32>,
|
||||
) {
|
||||
pipeline.draw(target, encoder, &clip_bounds, self.video_id);
|
||||
}
|
||||
}
|
||||
58
vendor/iced_video_player/src/shader.wgsl
vendored
Normal file
58
vendor/iced_video_player/src/shader.wgsl
vendored
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
}
|
||||
|
||||
struct Uniforms {
|
||||
rect: vec4<f32>,
|
||||
}
|
||||
|
||||
@group(0) @binding(0)
|
||||
var tex_y: texture_2d<f32>;
|
||||
|
||||
@group(0) @binding(1)
|
||||
var tex_uv: texture_2d<f32>;
|
||||
|
||||
@group(0) @binding(2)
|
||||
var s: sampler;
|
||||
|
||||
@group(0) @binding(3)
|
||||
var<uniform> uniforms: Uniforms;
|
||||
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) in_vertex_index: u32) -> VertexOutput {
|
||||
let quad = array<vec2<f32>, 6>(
|
||||
uniforms.rect.xy,
|
||||
uniforms.rect.zy,
|
||||
uniforms.rect.xw,
|
||||
uniforms.rect.zy,
|
||||
uniforms.rect.zw,
|
||||
uniforms.rect.xw,
|
||||
);
|
||||
|
||||
var out: VertexOutput;
|
||||
out.uv = vec2<f32>(0.0);
|
||||
out.uv.x = select(0.0, 2.0, in_vertex_index == 1u);
|
||||
out.uv.y = select(0.0, 2.0, in_vertex_index == 2u);
|
||||
out.position = vec4<f32>(out.uv * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0), 1.0, 1.0);
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
let yuv2r = vec3<f32>(1.164, 0.0, 1.596);
|
||||
let yuv2g = vec3<f32>(1.164, -0.391, -0.813);
|
||||
let yuv2b = vec3<f32>(1.164, 2.018, 0.0);
|
||||
|
||||
var yuv = vec3<f32>(0.0);
|
||||
yuv.x = textureSample(tex_y, s, in.uv).r - 0.0625;
|
||||
yuv.y = textureSample(tex_uv, s, in.uv).r - 0.5;
|
||||
yuv.z = textureSample(tex_uv, s, in.uv).g - 0.5;
|
||||
|
||||
var rgb = vec3<f32>(0.0);
|
||||
rgb.x = dot(yuv, yuv2r);
|
||||
rgb.y = dot(yuv, yuv2g);
|
||||
rgb.z = dot(yuv, yuv2b);
|
||||
|
||||
return vec4<f32>(rgb, 1.0);
|
||||
}
|
||||
664
vendor/iced_video_player/src/video.rs
vendored
Normal file
664
vendor/iced_video_player/src/video.rs
vendored
Normal file
|
|
@ -0,0 +1,664 @@
|
|||
use crate::Error;
|
||||
use cosmic::iced::widget::image as img;
|
||||
use gstreamer as gst;
|
||||
use gstreamer_app as gst_app;
|
||||
use gstreamer_app::prelude::*;
|
||||
use std::num::NonZeroU8;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const THUMBNAIL_FRAME_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
/// Position in the media.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum Position {
|
||||
/// Position based on time.
|
||||
///
|
||||
/// Not the most accurate format for videos.
|
||||
Time(Duration),
|
||||
/// Position based on nth frame.
|
||||
Frame(u64),
|
||||
}
|
||||
|
||||
impl From<Position> for gst::GenericFormattedValue {
|
||||
fn from(pos: Position) -> Self {
|
||||
match pos {
|
||||
Position::Time(t) => gst::ClockTime::from_nseconds(t.as_nanos() as _).into(),
|
||||
Position::Frame(f) => gst::format::Default::from_u64(f).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Duration> for Position {
|
||||
fn from(t: Duration) -> Self {
|
||||
Position::Time(t)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for Position {
|
||||
fn from(f: u64) -> Self {
|
||||
Position::Frame(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Frame(gst::Sample);
|
||||
|
||||
impl Frame {
|
||||
pub fn new() -> Self {
|
||||
Self(gst::Sample::builder().build())
|
||||
}
|
||||
pub fn store(&mut self, sample: gst::Sample) -> Option<()> {
|
||||
if sample.buffer().is_some() {
|
||||
self.0 = sample;
|
||||
Some(())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub fn readable(&self) -> Option<gst::BufferMap<gst::buffer::Readable>> {
|
||||
self.0.buffer().map(|x| x.map_readable().ok()).flatten()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Internal {
|
||||
pub(crate) id: u64,
|
||||
|
||||
pub(crate) bus: gst::Bus,
|
||||
pub(crate) source: gst::Pipeline,
|
||||
pub(crate) alive: Arc<AtomicBool>,
|
||||
pub(crate) worker: Option<std::thread::JoinHandle<()>>,
|
||||
|
||||
pub(crate) has_video: bool,
|
||||
pub(crate) width: i32,
|
||||
pub(crate) height: i32,
|
||||
pub(crate) framerate: f64,
|
||||
pub(crate) duration: Duration,
|
||||
pub(crate) speed: f64,
|
||||
pub(crate) sync_av: bool,
|
||||
|
||||
pub(crate) frame: Arc<Mutex<Frame>>,
|
||||
pub(crate) upload_frame: Arc<AtomicBool>,
|
||||
pub(crate) redrawing: Arc<AtomicBool>,
|
||||
pub(crate) last_frame_time: Arc<Mutex<Instant>>,
|
||||
pub(crate) looping: bool,
|
||||
pub(crate) is_eos: bool,
|
||||
pub(crate) restart_stream: bool,
|
||||
pub(crate) sync_av_avg: u64,
|
||||
pub(crate) sync_av_counter: u64,
|
||||
|
||||
pub(crate) subtitle_text: Arc<Mutex<Option<String>>>,
|
||||
pub(crate) upload_text: Arc<AtomicBool>,
|
||||
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
pub(crate) handle_opt: Option<img::Handle>,
|
||||
}
|
||||
|
||||
impl Internal {
|
||||
pub(crate) fn seek(&self, position: impl Into<Position>, accurate: bool) -> Result<(), Error> {
|
||||
let position = position.into();
|
||||
|
||||
// gstreamer complains if the start & end value types aren't the same
|
||||
match &position {
|
||||
Position::Time(_) => self.source.seek(
|
||||
self.speed,
|
||||
gst::SeekFlags::FLUSH
|
||||
| if accurate {
|
||||
gst::SeekFlags::ACCURATE
|
||||
} else {
|
||||
gst::SeekFlags::empty()
|
||||
},
|
||||
gst::SeekType::Set,
|
||||
gst::GenericFormattedValue::from(position),
|
||||
gst::SeekType::Set,
|
||||
gst::ClockTime::NONE,
|
||||
)?,
|
||||
Position::Frame(_) => self.source.seek(
|
||||
self.speed,
|
||||
gst::SeekFlags::FLUSH
|
||||
| if accurate {
|
||||
gst::SeekFlags::ACCURATE
|
||||
} else {
|
||||
gst::SeekFlags::empty()
|
||||
},
|
||||
gst::SeekType::Set,
|
||||
gst::GenericFormattedValue::from(position),
|
||||
gst::SeekType::Set,
|
||||
gst::format::Default::NONE,
|
||||
)?,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn set_speed(&mut self, speed: f64) -> Result<(), Error> {
|
||||
let Some(position) = self.source.query_position::<gst::ClockTime>() else {
|
||||
return Err(Error::Caps);
|
||||
};
|
||||
if speed > 0.0 {
|
||||
self.source.seek(
|
||||
speed,
|
||||
gst::SeekFlags::FLUSH | gst::SeekFlags::ACCURATE,
|
||||
gst::SeekType::Set,
|
||||
position,
|
||||
gst::SeekType::End,
|
||||
gst::ClockTime::from_seconds(0),
|
||||
)?;
|
||||
} else {
|
||||
self.source.seek(
|
||||
speed,
|
||||
gst::SeekFlags::FLUSH | gst::SeekFlags::ACCURATE,
|
||||
gst::SeekType::Set,
|
||||
gst::ClockTime::from_seconds(0),
|
||||
gst::SeekType::Set,
|
||||
position,
|
||||
)?;
|
||||
}
|
||||
self.speed = speed;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn restart_stream(&mut self) -> Result<(), Error> {
|
||||
self.is_eos = false;
|
||||
self.set_paused(false);
|
||||
self.seek(0, false)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn set_paused(&mut self, paused: bool) {
|
||||
self.source
|
||||
.set_state(if paused {
|
||||
gst::State::Paused
|
||||
} else {
|
||||
gst::State::Playing
|
||||
})
|
||||
.unwrap(/* state was changed in ctor; state errors caught there */);
|
||||
|
||||
// Set restart_stream flag to make the stream restart on the next Message::NextFrame
|
||||
if self.is_eos && !paused {
|
||||
self.restart_stream = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn paused(&self) -> bool {
|
||||
self.source.state(gst::ClockTime::ZERO).1 == gst::State::Paused
|
||||
}
|
||||
|
||||
/// Syncs audio with video when there is (inevitably) latency presenting the frame.
|
||||
pub(crate) fn set_av_offset(&mut self, offset: Duration) {
|
||||
if self.sync_av {
|
||||
self.sync_av_counter += 1;
|
||||
self.sync_av_avg = self.sync_av_avg * (self.sync_av_counter - 1) / self.sync_av_counter
|
||||
+ offset.as_nanos() as u64 / self.sync_av_counter;
|
||||
if self.sync_av_counter % 128 == 0 {
|
||||
self.source
|
||||
.set_property("av-offset", -(self.sync_av_avg as i64));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A multimedia video loaded from a URI (e.g., a local file path or HTTP stream).
|
||||
#[derive(Debug)]
|
||||
pub struct Video(pub(crate) RwLock<Internal>);
|
||||
|
||||
impl Drop for Video {
|
||||
fn drop(&mut self) {
|
||||
let inner = self.0.get_mut().expect("failed to lock");
|
||||
|
||||
inner
|
||||
.source
|
||||
.set_state(gst::State::Null)
|
||||
.expect("failed to set state");
|
||||
|
||||
inner.alive.store(false, Ordering::SeqCst);
|
||||
if let Some(worker) = inner.worker.take() {
|
||||
worker.join().expect("failed to stop video thread");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Video {
|
||||
/// Create a new video player from a given video which loads from `uri`.
|
||||
/// Note that live sources will report the duration to be zero.
|
||||
pub fn new(uri: &url::Url) -> Result<Self, Error> {
|
||||
gst::init()?;
|
||||
|
||||
let pipeline = format!(
|
||||
"playbin uri=\"{}\" text-sink=\"appsink name=iced_text sync=true caps=text/x-raw\" video-sink=\"videoscale ! videoconvert ! appsink name=iced_video drop=true caps=video/x-raw,format=NV12,pixel-aspect-ratio=1/1\"",
|
||||
uri.as_str()
|
||||
);
|
||||
let pipeline = gst::parse::launch(pipeline.as_ref())?
|
||||
.downcast::<gst::Pipeline>()
|
||||
.map_err(|_| Error::Cast)?;
|
||||
|
||||
let video_sink: gst::Element = pipeline.property("video-sink");
|
||||
let pad = video_sink.pads().first().cloned().unwrap();
|
||||
let pad = pad.dynamic_cast::<gst::GhostPad>().unwrap();
|
||||
let bin = pad
|
||||
.parent_element()
|
||||
.unwrap()
|
||||
.downcast::<gst::Bin>()
|
||||
.unwrap();
|
||||
let video_sink = bin.by_name("iced_video").unwrap();
|
||||
let video_sink = video_sink.downcast::<gst_app::AppSink>().unwrap();
|
||||
|
||||
let text_sink: gst::Element = pipeline.property("text-sink");
|
||||
let text_sink = text_sink.downcast::<gst_app::AppSink>().unwrap();
|
||||
|
||||
Self::from_gst_pipeline(pipeline, video_sink, Some(text_sink))
|
||||
}
|
||||
|
||||
/// Creates a new video based on an existing GStreamer pipeline and appsink.
|
||||
/// Expects an `appsink` plugin with `caps=video/x-raw,format=NV12`.
|
||||
///
|
||||
/// An optional `text_sink` can be provided, which enables subtitle messages
|
||||
/// to be emitted.
|
||||
///
|
||||
/// **Note:** Many functions of [`Video`] assume a `playbin` pipeline.
|
||||
/// Non-`playbin` pipelines given here may not have full functionality.
|
||||
pub fn from_gst_pipeline(
|
||||
pipeline: gst::Pipeline,
|
||||
video_sink: gst_app::AppSink,
|
||||
text_sink: Option<gst_app::AppSink>,
|
||||
) -> Result<Self, Error> {
|
||||
gst::init()?;
|
||||
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
|
||||
let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
let pad = video_sink.pads().first().cloned().unwrap();
|
||||
|
||||
pipeline.set_state(gst::State::Playing)?;
|
||||
|
||||
// wait for up to 5 seconds until the decoder gets the source capabilities
|
||||
pipeline.state(gst::ClockTime::from_seconds(5)).0?;
|
||||
|
||||
// extract resolution and framerate
|
||||
// TODO(jazzfool): maybe we want to extract some other information too?
|
||||
let (width, height, framerate, has_video) = if let Some(caps) = pad.current_caps() {
|
||||
let s = caps.structure(0).ok_or(Error::Caps)?;
|
||||
let width = s.get::<i32>("width").map_err(|_| Error::Caps)?;
|
||||
let height = s.get::<i32>("height").map_err(|_| Error::Caps)?;
|
||||
// resolution should be mod4
|
||||
let width = ((width + 4 - 1) / 4) * 4;
|
||||
let framerate = s
|
||||
.get::<gst::Fraction>("framerate")
|
||||
.map_err(|_| Error::Caps)?;
|
||||
let framerate = framerate.numer() as f64 / framerate.denom() as f64;
|
||||
(width, height, framerate, true)
|
||||
} else {
|
||||
log::warn!("Video caps not found, falling back to audio only");
|
||||
(0, 0, 4.0, false)
|
||||
};
|
||||
|
||||
if framerate.is_nan()
|
||||
|| framerate.is_infinite()
|
||||
|| framerate < 0.0
|
||||
|| framerate.abs() < f64::EPSILON
|
||||
{
|
||||
return Err(Error::Framerate(framerate));
|
||||
}
|
||||
|
||||
let duration = Duration::from_nanos(
|
||||
pipeline
|
||||
.query_duration::<gst::ClockTime>()
|
||||
.map(|duration| duration.nseconds())
|
||||
.unwrap_or(0),
|
||||
);
|
||||
|
||||
let sync_av = pipeline.has_property("av-offset");
|
||||
|
||||
// NV12 = 12bpp
|
||||
let frame = Arc::new(Mutex::new(Frame::new()));
|
||||
let upload_frame = Arc::new(AtomicBool::new(false));
|
||||
let alive = Arc::new(AtomicBool::new(true));
|
||||
let last_frame_time = Arc::new(Mutex::new(Instant::now()));
|
||||
|
||||
let frame_ref = Arc::clone(&frame);
|
||||
let upload_frame_ref = Arc::clone(&upload_frame);
|
||||
let alive_ref = Arc::clone(&alive);
|
||||
let last_frame_time_ref = Arc::clone(&last_frame_time);
|
||||
|
||||
let subtitle_text = Arc::new(Mutex::new(None));
|
||||
let upload_text = Arc::new(AtomicBool::new(false));
|
||||
let subtitle_text_ref = Arc::clone(&subtitle_text);
|
||||
let upload_text_ref = Arc::clone(&upload_text);
|
||||
|
||||
let pipeline_ref = pipeline.clone();
|
||||
|
||||
let worker = std::thread::spawn(move || {
|
||||
let mut clear_subtitles_at = None;
|
||||
|
||||
while alive_ref.load(Ordering::Acquire) {
|
||||
match (|| -> Result<(), gst::FlowError> {
|
||||
let sample =
|
||||
if pipeline_ref.state(gst::ClockTime::ZERO).1 != gst::State::Playing {
|
||||
video_sink
|
||||
.try_pull_preroll(gst::ClockTime::from_mseconds(16))
|
||||
.ok_or(gst::FlowError::Eos)?
|
||||
} else {
|
||||
video_sink
|
||||
.try_pull_sample(gst::ClockTime::from_mseconds(16))
|
||||
.ok_or(gst::FlowError::Eos)?
|
||||
};
|
||||
|
||||
*last_frame_time_ref
|
||||
.lock()
|
||||
.map_err(|_| gst::FlowError::Error)? = Instant::now();
|
||||
|
||||
let buffer = sample.buffer().ok_or(gst::FlowError::Error)?;
|
||||
let pts = buffer.pts().unwrap_or_default();
|
||||
{
|
||||
let mut frame_guard =
|
||||
frame_ref.lock().map_err(|_| gst::FlowError::Error)?;
|
||||
*frame_guard = Frame(sample);
|
||||
}
|
||||
|
||||
upload_frame_ref.swap(true, Ordering::SeqCst);
|
||||
|
||||
if let Some(at) = clear_subtitles_at {
|
||||
if pts >= at {
|
||||
*subtitle_text_ref
|
||||
.lock()
|
||||
.map_err(|_| gst::FlowError::Error)? = None;
|
||||
upload_text_ref.store(true, Ordering::SeqCst);
|
||||
clear_subtitles_at = None;
|
||||
}
|
||||
}
|
||||
|
||||
let text = text_sink
|
||||
.as_ref()
|
||||
.and_then(|sink| sink.try_pull_sample(gst::ClockTime::from_seconds(0)));
|
||||
if let Some(text) = text {
|
||||
let text = text.buffer().ok_or(gst::FlowError::Error)?;
|
||||
let pts = text.pts().unwrap_or_default();
|
||||
let duration = text.duration().unwrap_or(gst::ClockTime::ZERO);
|
||||
let map = text.map_readable().map_err(|_| gst::FlowError::Error)?;
|
||||
|
||||
let text = html_escape::decode_html_entities(
|
||||
std::str::from_utf8(map.as_slice())
|
||||
.map_err(|_| gst::FlowError::Error)?,
|
||||
)
|
||||
.to_string();
|
||||
*subtitle_text_ref
|
||||
.lock()
|
||||
.map_err(|_| gst::FlowError::Error)? = Some(text);
|
||||
upload_text_ref.store(true, Ordering::SeqCst);
|
||||
|
||||
clear_subtitles_at = Some(pts + duration);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})() {
|
||||
Ok(()) => {}
|
||||
Err(gst::FlowError::Eos) => {
|
||||
if !has_video {
|
||||
// Simulate frame upload when there is no video stream
|
||||
upload_frame_ref.swap(true, Ordering::SeqCst);
|
||||
std::thread::sleep(Duration::from_secs_f64(1.0 / framerate));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("error pulling frame: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Video(RwLock::new(Internal {
|
||||
id,
|
||||
|
||||
bus: pipeline.bus().unwrap(),
|
||||
source: pipeline,
|
||||
alive,
|
||||
worker: Some(worker),
|
||||
|
||||
has_video,
|
||||
width,
|
||||
height,
|
||||
framerate,
|
||||
duration,
|
||||
speed: 1.0,
|
||||
sync_av,
|
||||
|
||||
frame,
|
||||
upload_frame,
|
||||
redrawing: Arc::new(AtomicBool::new(false)),
|
||||
last_frame_time,
|
||||
looping: false,
|
||||
is_eos: false,
|
||||
restart_stream: false,
|
||||
sync_av_avg: 0,
|
||||
sync_av_counter: 0,
|
||||
|
||||
subtitle_text,
|
||||
upload_text,
|
||||
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
handle_opt: None,
|
||||
})))
|
||||
}
|
||||
|
||||
pub(crate) fn read(&self) -> impl Deref<Target = Internal> + '_ {
|
||||
self.0.read().expect("lock")
|
||||
}
|
||||
|
||||
pub(crate) fn write(&self) -> impl DerefMut<Target = Internal> + '_ {
|
||||
self.0.write().expect("lock")
|
||||
}
|
||||
|
||||
pub(crate) fn get_mut(&mut self) -> impl DerefMut<Target = Internal> + '_ {
|
||||
self.0.get_mut().expect("lock")
|
||||
}
|
||||
|
||||
pub fn has_video(&self) -> bool {
|
||||
self.read().has_video
|
||||
}
|
||||
|
||||
/// Get the size/resolution of the video as `(width, height)`.
|
||||
pub fn size(&self) -> (i32, i32) {
|
||||
(self.read().width, self.read().height)
|
||||
}
|
||||
|
||||
/// Get the framerate of the video as frames per second.
|
||||
pub fn framerate(&self) -> f64 {
|
||||
self.read().framerate
|
||||
}
|
||||
|
||||
/// Set the volume multiplier of the audio.
|
||||
/// `0.0` = 0% volume, `1.0` = 100% volume.
|
||||
///
|
||||
/// This uses a linear scale, for example `0.5` is perceived as half as loud.
|
||||
pub fn set_volume(&mut self, volume: f64) {
|
||||
self.get_mut().source.set_property("volume", volume);
|
||||
self.set_muted(self.muted()); // for some reason gstreamer unmutes when changing volume?
|
||||
}
|
||||
|
||||
/// Get the volume multiplier of the audio.
|
||||
pub fn volume(&self) -> f64 {
|
||||
self.read().source.property("volume")
|
||||
}
|
||||
|
||||
/// Set if the audio is muted or not, without changing the volume.
|
||||
pub fn set_muted(&mut self, muted: bool) {
|
||||
self.get_mut().source.set_property("mute", muted);
|
||||
}
|
||||
|
||||
/// Get if the audio is muted or not.
|
||||
pub fn muted(&self) -> bool {
|
||||
self.read().source.property("mute")
|
||||
}
|
||||
|
||||
/// Get if the stream ended or not.
|
||||
pub fn eos(&self) -> bool {
|
||||
self.read().is_eos
|
||||
}
|
||||
|
||||
/// Get if the media will loop or not.
|
||||
pub fn looping(&self) -> bool {
|
||||
self.read().looping
|
||||
}
|
||||
|
||||
/// Set if the media will loop or not.
|
||||
pub fn set_looping(&mut self, looping: bool) {
|
||||
self.get_mut().looping = looping;
|
||||
}
|
||||
|
||||
/// Set if the media is paused or not.
|
||||
pub fn set_paused(&mut self, paused: bool) {
|
||||
self.get_mut().set_paused(paused)
|
||||
}
|
||||
|
||||
/// Get if the media is paused or not.
|
||||
pub fn paused(&self) -> bool {
|
||||
self.read().paused()
|
||||
}
|
||||
|
||||
/// Jumps to a specific position in the media.
|
||||
/// Passing `true` to the `accurate` parameter will result in more accurate seeking,
|
||||
/// however, it is also slower. For most seeks (e.g., scrubbing) this is not needed.
|
||||
pub fn seek(&mut self, position: impl Into<Position>, accurate: bool) -> Result<(), Error> {
|
||||
self.get_mut().seek(position, accurate)
|
||||
}
|
||||
|
||||
/// Set the playback speed of the media.
|
||||
/// The default speed is `1.0`.
|
||||
pub fn set_speed(&mut self, speed: f64) -> Result<(), Error> {
|
||||
self.get_mut().set_speed(speed)
|
||||
}
|
||||
|
||||
/// Get the current playback speed.
|
||||
pub fn speed(&self) -> f64 {
|
||||
self.read().speed
|
||||
}
|
||||
|
||||
/// Get the current playback position in time.
|
||||
pub fn position(&self) -> Duration {
|
||||
Duration::from_nanos(
|
||||
self.read()
|
||||
.source
|
||||
.query_position::<gst::ClockTime>()
|
||||
.map_or(0, |pos| pos.nseconds()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the media duration.
|
||||
pub fn duration(&self) -> Duration {
|
||||
self.read().duration
|
||||
}
|
||||
|
||||
/// Restarts a stream; seeks to the first frame and unpauses, sets the `eos` flag to false.
|
||||
pub fn restart_stream(&mut self) -> Result<(), Error> {
|
||||
self.get_mut().restart_stream()
|
||||
}
|
||||
|
||||
/// Set the subtitle URL to display.
|
||||
pub fn set_subtitle_url(&mut self, url: &url::Url) -> Result<(), Error> {
|
||||
let paused = self.paused();
|
||||
let mut inner = self.get_mut();
|
||||
inner.source.set_state(gst::State::Ready)?;
|
||||
inner.source.set_property("suburi", url.as_str());
|
||||
inner.set_paused(paused);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the current subtitle URL.
|
||||
pub fn subtitle_url(&self) -> Option<url::Url> {
|
||||
url::Url::parse(&self.read().source.property::<String>("suburi")).ok()
|
||||
}
|
||||
|
||||
/// Get the underlying GStreamer pipeline.
|
||||
pub fn pipeline(&self) -> gst::Pipeline {
|
||||
self.read().source.clone()
|
||||
}
|
||||
|
||||
/// Generates a list of thumbnails based on a set of positions in the media, downscaled by a given factor.
|
||||
///
|
||||
/// Slow; only needs to be called once for each instance.
|
||||
/// It's best to call this at the very start of playback, otherwise the position may shift.
|
||||
pub fn thumbnails<I>(
|
||||
&mut self,
|
||||
positions: I,
|
||||
downscale: NonZeroU8,
|
||||
) -> Result<Vec<img::Handle>, Error>
|
||||
where
|
||||
I: IntoIterator<Item = Position>,
|
||||
{
|
||||
let downscale = u8::from(downscale) as u32;
|
||||
|
||||
let paused = self.paused();
|
||||
let muted = self.muted();
|
||||
let pos = self.position();
|
||||
|
||||
self.set_paused(false);
|
||||
self.set_muted(true);
|
||||
|
||||
let out = {
|
||||
let inner = self.read();
|
||||
let width = inner.width;
|
||||
let height = inner.height;
|
||||
positions
|
||||
.into_iter()
|
||||
.map(|pos| {
|
||||
inner.seek(pos, true)?;
|
||||
|
||||
inner.upload_frame.store(false, Ordering::SeqCst);
|
||||
|
||||
let started = Instant::now();
|
||||
while !inner.upload_frame.load(Ordering::SeqCst) {
|
||||
if started.elapsed() >= THUMBNAIL_FRAME_TIMEOUT {
|
||||
return Err(Error::Sync);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
let frame_guard = inner.frame.lock().map_err(|_| Error::Lock)?;
|
||||
let frame = frame_guard.readable().ok_or(Error::Lock)?;
|
||||
|
||||
Ok(img::Handle::from_rgba(
|
||||
inner.width as u32 / downscale,
|
||||
inner.height as u32 / downscale,
|
||||
yuv_to_rgba(frame.as_slice(), width as _, height as _, downscale),
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
self.set_paused(paused);
|
||||
self.set_muted(muted);
|
||||
self.seek(pos, true)?;
|
||||
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn yuv_to_rgba(yuv: &[u8], width: u32, height: u32, downscale: u32) -> Vec<u8> {
|
||||
let uv_start = width * height;
|
||||
let mut rgba = vec![];
|
||||
|
||||
for y in 0..height / downscale {
|
||||
for x in 0..width / downscale {
|
||||
let x_src = x * downscale;
|
||||
let y_src = y * downscale;
|
||||
|
||||
let uv_i = uv_start + width * (y_src / 2) + x_src / 2 * 2;
|
||||
|
||||
let y = yuv[(y_src * width + x_src) as usize] as f32;
|
||||
let u = yuv[uv_i as usize] as f32;
|
||||
let v = yuv[(uv_i + 1) as usize] as f32;
|
||||
|
||||
let r = 1.164 * (y - 16.0) + 1.596 * (v - 128.0);
|
||||
let g = 1.164 * (y - 16.0) - 0.813 * (v - 128.0) - 0.391 * (u - 128.0);
|
||||
let b = 1.164 * (y - 16.0) + 2.018 * (u - 128.0);
|
||||
|
||||
rgba.push(r as u8);
|
||||
rgba.push(g as u8);
|
||||
rgba.push(b as u8);
|
||||
rgba.push(0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
rgba
|
||||
}
|
||||
476
vendor/iced_video_player/src/video_player.rs
vendored
Normal file
476
vendor/iced_video_player/src/video_player.rs
vendored
Normal file
|
|
@ -0,0 +1,476 @@
|
|||
use crate::{gst, gst_pbutils, video::Video};
|
||||
use cosmic::iced::{
|
||||
self,
|
||||
advanced::{self, layout, widget, Widget},
|
||||
mouse, Element,
|
||||
};
|
||||
use gstreamer_app::prelude::*;
|
||||
use log::error;
|
||||
use std::{marker::PhantomData, sync::atomic::Ordering, time::Duration};
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
use crate::pipeline::VideoPrimitive;
|
||||
#[cfg(feature = "wgpu")]
|
||||
use iced_wgpu::primitive::Renderer as PrimitiveRenderer;
|
||||
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
use crate::video::yuv_to_rgba;
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
use cosmic::iced::advanced::image::Renderer as ImageRenderer;
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
trait PrimitiveRenderer: ImageRenderer<Handle = advanced::image::Handle> {}
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
impl PrimitiveRenderer for iced::Renderer {}
|
||||
|
||||
/// Video player widget which displays the current frame of a [`Video`](crate::Video).
|
||||
pub struct VideoPlayer<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer>
|
||||
where
|
||||
Renderer: PrimitiveRenderer,
|
||||
{
|
||||
video: &'a Video,
|
||||
content_fit: iced::ContentFit,
|
||||
width: iced::Length,
|
||||
height: iced::Length,
|
||||
mouse_hidden: bool,
|
||||
on_duration_changed: Option<Box<dyn Fn(Duration) -> Message + 'a>>,
|
||||
on_end_of_stream: Option<Message>,
|
||||
on_new_frame: Option<Message>,
|
||||
on_subtitle_text: Option<Box<dyn Fn(Option<String>) -> Message + 'a>>,
|
||||
on_error: Option<Box<dyn Fn(glib::Error) -> Message + 'a>>,
|
||||
on_missing_plugin: Option<Box<dyn Fn(gst::Message) -> Message + 'a>>,
|
||||
on_tags: Option<Box<dyn Fn(gst::TagList) -> Message + 'a>>,
|
||||
on_warning: Option<Box<dyn Fn(glib::Error) -> Message + 'a>>,
|
||||
id: Option<cosmic::widget::Id>,
|
||||
_phantom: PhantomData<(Theme, Renderer)>,
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> VideoPlayer<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: PrimitiveRenderer,
|
||||
{
|
||||
/// Creates a new video player widget for a given video.
|
||||
pub fn new(video: &'a Video) -> Self {
|
||||
VideoPlayer {
|
||||
video,
|
||||
content_fit: iced::ContentFit::Contain,
|
||||
width: iced::Length::Shrink,
|
||||
height: iced::Length::Shrink,
|
||||
mouse_hidden: false,
|
||||
on_duration_changed: None,
|
||||
on_end_of_stream: None,
|
||||
on_new_frame: None,
|
||||
on_subtitle_text: None,
|
||||
on_error: None,
|
||||
on_missing_plugin: None,
|
||||
on_tags: None,
|
||||
on_warning: None,
|
||||
_phantom: Default::default(),
|
||||
id: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the ID of the `VideoPlayer`.
|
||||
pub fn id(mut self, id: cosmic::widget::Id) -> Self {
|
||||
self.id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the `VideoPlayer` boundaries.
|
||||
pub fn width(self, width: impl Into<iced::Length>) -> Self {
|
||||
VideoPlayer {
|
||||
width: width.into(),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the height of the `VideoPlayer` boundaries.
|
||||
pub fn height(self, height: impl Into<iced::Length>) -> Self {
|
||||
VideoPlayer {
|
||||
height: height.into(),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the `ContentFit` of the `VideoPlayer`.
|
||||
pub fn content_fit(self, content_fit: iced::ContentFit) -> Self {
|
||||
VideoPlayer {
|
||||
content_fit,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mouse_hidden(self, mouse_hidden: bool) -> Self {
|
||||
VideoPlayer {
|
||||
mouse_hidden,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_duration_changed<F>(self, on_duration_changed: F) -> Self
|
||||
where
|
||||
F: 'a + Fn(Duration) -> Message,
|
||||
{
|
||||
VideoPlayer {
|
||||
on_duration_changed: Some(Box::new(on_duration_changed)),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Message to send when the video reaches the end of stream (i.e., the video ends).
|
||||
pub fn on_end_of_stream(self, on_end_of_stream: Message) -> Self {
|
||||
VideoPlayer {
|
||||
on_end_of_stream: Some(on_end_of_stream),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Message to send when the video receives a new frame.
|
||||
pub fn on_new_frame(self, on_new_frame: Message) -> Self {
|
||||
VideoPlayer {
|
||||
on_new_frame: Some(on_new_frame),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Message to send when the video receives a new frame.
|
||||
pub fn on_subtitle_text<F>(self, on_subtitle_text: F) -> Self
|
||||
where
|
||||
F: 'a + Fn(Option<String>) -> Message,
|
||||
{
|
||||
VideoPlayer {
|
||||
on_subtitle_text: Some(Box::new(on_subtitle_text)),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Message to send when the video playback encounters an error.
|
||||
pub fn on_error<F>(self, on_error: F) -> Self
|
||||
where
|
||||
F: 'a + Fn(glib::Error) -> Message,
|
||||
{
|
||||
VideoPlayer {
|
||||
on_error: Some(Box::new(on_error)),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_missing_plugin<F>(self, on_missing_plugin: F) -> Self
|
||||
where
|
||||
F: 'a + Fn(gst::Message) -> Message,
|
||||
{
|
||||
VideoPlayer {
|
||||
on_missing_plugin: Some(Box::new(on_missing_plugin)),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_tags<F>(self, on_tags: F) -> Self
|
||||
where
|
||||
F: 'a + Fn(gst::TagList) -> Message,
|
||||
{
|
||||
VideoPlayer {
|
||||
on_tags: Some(Box::new(on_tags)),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_warning<F>(self, on_warning: F) -> Self
|
||||
where
|
||||
F: 'a + Fn(glib::Error) -> Message,
|
||||
{
|
||||
VideoPlayer {
|
||||
on_warning: Some(Box::new(on_warning)),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for VideoPlayer<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: Clone,
|
||||
Renderer: PrimitiveRenderer,
|
||||
{
|
||||
fn size(&self) -> iced::Size<iced::Length> {
|
||||
iced::Size {
|
||||
width: iced::Length::Shrink,
|
||||
height: iced::Length::Shrink,
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_tree: &mut widget::Tree,
|
||||
_renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let (video_width, video_height) = self.video.size();
|
||||
|
||||
// based on `Image::layout`
|
||||
let image_size = iced::Size::new(video_width as f32, video_height as f32);
|
||||
let raw_size = limits.resolve(self.width, self.height, image_size);
|
||||
let full_size = self.content_fit.fit(image_size, raw_size);
|
||||
let final_size = iced::Size {
|
||||
width: match self.width {
|
||||
iced::Length::Shrink => f32::min(raw_size.width, full_size.width),
|
||||
_ => raw_size.width,
|
||||
},
|
||||
height: match self.height {
|
||||
iced::Length::Shrink => f32::min(raw_size.height, full_size.height),
|
||||
_ => raw_size.height,
|
||||
},
|
||||
};
|
||||
|
||||
layout::Node::new(final_size)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
_tree: &widget::Tree,
|
||||
renderer: &mut Renderer,
|
||||
_theme: &Theme,
|
||||
_style: &advanced::renderer::Style,
|
||||
layout: advanced::Layout<'_>,
|
||||
_cursor: advanced::mouse::Cursor,
|
||||
_viewport: &iced::Rectangle,
|
||||
) {
|
||||
let mut inner = self.video.write();
|
||||
|
||||
// bounds based on `Image::draw`
|
||||
let image_size = iced::Size::new(inner.width as f32, inner.height as f32);
|
||||
let bounds = layout.bounds();
|
||||
let adjusted_fit = self.content_fit.fit(image_size, bounds.size());
|
||||
let scale = iced::Vector::new(
|
||||
adjusted_fit.width / image_size.width,
|
||||
adjusted_fit.height / image_size.height,
|
||||
);
|
||||
let final_size = iced::Size::new(image_size.width * scale.x, image_size.height * scale.y);
|
||||
|
||||
let position = match self.content_fit {
|
||||
iced::ContentFit::None => iced::Point::new(
|
||||
bounds.x + (image_size.width - adjusted_fit.width) / 2.0,
|
||||
bounds.y + (image_size.height - adjusted_fit.height) / 2.0,
|
||||
),
|
||||
_ => iced::Point::new(
|
||||
bounds.center_x() - final_size.width / 2.0,
|
||||
bounds.center_y() - final_size.height / 2.0,
|
||||
),
|
||||
};
|
||||
|
||||
let drawing_bounds = iced::Rectangle::new(position, final_size);
|
||||
|
||||
let upload_frame = inner.upload_frame.swap(false, Ordering::SeqCst);
|
||||
inner.redrawing.store(false, Ordering::SeqCst);
|
||||
|
||||
if upload_frame {
|
||||
let last_frame_time = inner
|
||||
.last_frame_time
|
||||
.lock()
|
||||
.map(|time| *time)
|
||||
.unwrap_or_else(|_| Instant::now());
|
||||
inner.set_av_offset(Instant::now() - last_frame_time);
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
renderer.draw_primitive(
|
||||
drawing_bounds,
|
||||
VideoPrimitive::new(
|
||||
inner.id,
|
||||
Arc::clone(&inner.alive),
|
||||
Arc::clone(&inner.frame),
|
||||
(inner.width as _, inner.height as _),
|
||||
upload_frame,
|
||||
),
|
||||
);
|
||||
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
{
|
||||
if upload_frame {
|
||||
let mut opt = None;
|
||||
{
|
||||
let yuv_data_opt = match inner.frame.lock() {
|
||||
Ok(frame) => Some(frame),
|
||||
Err(_err) => None,
|
||||
};
|
||||
if let Some(yuv_data) = yuv_data_opt.as_ref().and_then(|d| d.readable()) {
|
||||
//TODO: convert on worker thread?
|
||||
let rgba_data =
|
||||
yuv_to_rgba(&yuv_data, inner.width as _, inner.height as _, 1);
|
||||
opt = Some(advanced::image::Handle::from_rgba(
|
||||
inner.width as _,
|
||||
inner.height as _,
|
||||
rgba_data,
|
||||
))
|
||||
};
|
||||
}
|
||||
inner.handle_opt = opt;
|
||||
}
|
||||
if let Some(handle) = &inner.handle_opt {
|
||||
use cosmic::iced::Radians;
|
||||
|
||||
renderer.draw_image(
|
||||
handle.clone(),
|
||||
advanced::image::FilterMethod::Nearest,
|
||||
drawing_bounds,
|
||||
Radians(0.),
|
||||
1.0,
|
||||
[0.0; 4],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
_state: &mut widget::Tree,
|
||||
event: &iced::Event,
|
||||
_layout: advanced::Layout<'_>,
|
||||
_cursor: advanced::mouse::Cursor,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn advanced::Clipboard,
|
||||
shell: &mut advanced::Shell<'_, Message>,
|
||||
_viewport: &iced::Rectangle,
|
||||
) {
|
||||
let mut inner = self.video.write();
|
||||
|
||||
if let iced::Event::Window(iced::window::Event::RedrawRequested(_)) = event {
|
||||
if inner.restart_stream || (!inner.is_eos && !inner.paused()) {
|
||||
let mut restart_stream = false;
|
||||
if inner.restart_stream {
|
||||
restart_stream = true;
|
||||
// Set flag to false to avoid potentially multiple seeks
|
||||
inner.restart_stream = false;
|
||||
}
|
||||
let mut eos_pause = false;
|
||||
|
||||
while let Some(msg) = inner.bus.pop_filtered(&[
|
||||
gst::MessageType::DurationChanged,
|
||||
gst::MessageType::Error,
|
||||
gst::MessageType::Element,
|
||||
gst::MessageType::Eos,
|
||||
gst::MessageType::Tag,
|
||||
gst::MessageType::Warning,
|
||||
]) {
|
||||
match msg.view() {
|
||||
gst::MessageView::DurationChanged(_) => {
|
||||
inner.duration = Duration::from_nanos(
|
||||
inner
|
||||
.source
|
||||
.query_duration::<gst::ClockTime>()
|
||||
.map(|duration| duration.nseconds())
|
||||
.unwrap_or(0),
|
||||
);
|
||||
if let Some(ref on_duration_changed) = self.on_duration_changed {
|
||||
shell.publish(on_duration_changed(inner.duration));
|
||||
}
|
||||
}
|
||||
gst::MessageView::Error(err) => {
|
||||
error!("bus returned an error: {err}");
|
||||
if let Some(ref on_error) = self.on_error {
|
||||
shell.publish(on_error(err.error()))
|
||||
};
|
||||
}
|
||||
gst::MessageView::Element(element) => {
|
||||
if gst_pbutils::MissingPluginMessage::is(&element) {
|
||||
if let Some(ref on_missing_plugin) = self.on_missing_plugin {
|
||||
shell.publish(on_missing_plugin(element.copy()));
|
||||
}
|
||||
}
|
||||
}
|
||||
gst::MessageView::Eos(_eos) => {
|
||||
if let Some(on_end_of_stream) = self.on_end_of_stream.clone() {
|
||||
shell.publish(on_end_of_stream);
|
||||
}
|
||||
if inner.looping {
|
||||
restart_stream = true;
|
||||
} else {
|
||||
eos_pause = true;
|
||||
}
|
||||
}
|
||||
gst::MessageView::Tag(tag_msg) => {
|
||||
if let Some(ref on_tags) = self.on_tags {
|
||||
shell.publish(on_tags(tag_msg.tags()));
|
||||
}
|
||||
}
|
||||
gst::MessageView::Warning(warn) => {
|
||||
log::warn!("bus returned a warning: {warn}");
|
||||
if let Some(ref on_warning) = self.on_warning {
|
||||
shell.publish(on_warning(warn.error()));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't run eos_pause if restart_stream is true; fixes "pausing" after restarting a stream
|
||||
if restart_stream {
|
||||
if let Err(err) = inner.restart_stream() {
|
||||
error!("cannot restart stream (can't seek): {err:#?}");
|
||||
}
|
||||
} else if eos_pause {
|
||||
inner.is_eos = true;
|
||||
inner.set_paused(true);
|
||||
}
|
||||
|
||||
if !inner.redrawing.load(Ordering::SeqCst)
|
||||
&& inner.upload_frame.load(Ordering::SeqCst)
|
||||
{
|
||||
if let Some(on_new_frame) = self.on_new_frame.clone() {
|
||||
inner.redrawing.store(true, Ordering::SeqCst);
|
||||
shell.publish(on_new_frame);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(on_subtitle_text) = &self.on_subtitle_text {
|
||||
if inner.upload_text.swap(false, Ordering::SeqCst) {
|
||||
if let Ok(text) = inner.subtitle_text.try_lock() {
|
||||
shell.publish(on_subtitle_text(text.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
shell.request_redraw();
|
||||
} else {
|
||||
shell.request_redraw_at(iced::window::RedrawRequest::At(
|
||||
Instant::now() + Duration::from_millis(32),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
_tree: &widget::Tree,
|
||||
_layout: advanced::Layout<'_>,
|
||||
_cursor_position: mouse::Cursor,
|
||||
_viewport: &iced::Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
if self.mouse_hidden {
|
||||
mouse::Interaction::Hidden
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn set_id(&mut self, id: widget::Id) {
|
||||
self.id = Some(id);
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<widget::Id> {
|
||||
self.id.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> From<VideoPlayer<'a, Message, Theme, Renderer>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
Message: 'a + Clone,
|
||||
Theme: 'a,
|
||||
Renderer: 'a + PrimitiveRenderer,
|
||||
{
|
||||
fn from(video_player: VideoPlayer<'a, Message, Theme, Renderer>) -> Self {
|
||||
Self::new(video_player)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue