Remove ffmpeg implementation
This commit is contained in:
parent
d61426957d
commit
f10350c7ec
11 changed files with 605 additions and 2304 deletions
320
Cargo.lock
generated
320
Cargo.lock
generated
|
|
@ -148,28 +148,6 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3aa2999eb46af81abb65c2d30d446778d7e613b60bbf4e174a027e80f90a3c14"
|
||||
|
||||
[[package]]
|
||||
name = "alsa"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43"
|
||||
dependencies = [
|
||||
"alsa-sys",
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alsa-sys"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android-activity"
|
||||
version = "0.5.2"
|
||||
|
|
@ -281,9 +259,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ashpd"
|
||||
version = "0.9.1"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfe7e0dd0ac5a401dc116ed9f9119cf9decc625600474cb41f0fc0a0050abc9a"
|
||||
checksum = "4d43c03d9e36dd40cab48435be0b09646da362c278223ca535493877b2c1dee9"
|
||||
dependencies = [
|
||||
"enumflags2",
|
||||
"futures-channel",
|
||||
|
|
@ -624,44 +602,6 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.64.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
"peeking_take_while",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.5.3"
|
||||
|
|
@ -868,15 +808,6 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-expr"
|
||||
version = "0.17.0"
|
||||
|
|
@ -919,17 +850,6 @@ dependencies = [
|
|||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "5.4.0"
|
||||
|
|
@ -1124,30 +1044,10 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coreaudio-rs"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation-sys",
|
||||
"coreaudio-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coreaudio-sys"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ce857aa0b77d77287acc1ac3e37a05a8c95a2af3647d23b15f263bdaeb7562b"
|
||||
dependencies = [
|
||||
"bindgen 0.70.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-config"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#5306649be1cfb6c384da11e2ab25cafc4be79b14"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#8b37f658e2d0885b6972952cbf0e606d188ba2b7"
|
||||
dependencies = [
|
||||
"atomicwrites",
|
||||
"cosmic-config-derive",
|
||||
|
|
@ -1166,7 +1066,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "cosmic-config-derive"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#5306649be1cfb6c384da11e2ab25cafc4be79b14"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#8b37f658e2d0885b6972952cbf0e606d188ba2b7"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
|
|
@ -1176,17 +1076,13 @@ dependencies = [
|
|||
name = "cosmic-player"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cpal",
|
||||
"env_logger",
|
||||
"ffmpeg-next",
|
||||
"i18n-embed",
|
||||
"i18n-embed-fl",
|
||||
"iced_video_player",
|
||||
"lazy_static",
|
||||
"lexopt",
|
||||
"libcosmic",
|
||||
"log",
|
||||
"paste",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"smol_str",
|
||||
|
|
@ -1220,7 +1116,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "cosmic-theme"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#5306649be1cfb6c384da11e2ab25cafc4be79b14"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#8b37f658e2d0885b6972952cbf0e606d188ba2b7"
|
||||
dependencies = [
|
||||
"almost",
|
||||
"cosmic-config",
|
||||
|
|
@ -1234,29 +1130,6 @@ dependencies = [
|
|||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpal"
|
||||
version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
|
||||
dependencies = [
|
||||
"alsa",
|
||||
"core-foundation-sys",
|
||||
"coreaudio-rs",
|
||||
"dasp_sample",
|
||||
"jni",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"mach2",
|
||||
"ndk",
|
||||
"ndk-context",
|
||||
"oboe",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows 0.54.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.14"
|
||||
|
|
@ -1411,12 +1284,6 @@ dependencies = [
|
|||
"parking_lot_core 0.9.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dasp_sample"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
|
||||
|
||||
[[package]]
|
||||
name = "data-url"
|
||||
version = "0.3.1"
|
||||
|
|
@ -1740,31 +1607,6 @@ dependencies = [
|
|||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ffmpeg-next"
|
||||
version = "6.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e72c72e8dcf638fb0fb03f033a954691662b5dabeaa3f85a6607d101569fccd"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"ffmpeg-sys-next",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ffmpeg-sys-next"
|
||||
version = "6.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2529ad916d08c3562c754c21bc9b17a26c7882c0f5706cc2cd69472175f1620"
|
||||
dependencies = [
|
||||
"bindgen 0.64.0",
|
||||
"cc",
|
||||
"libc",
|
||||
"num_cpus",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.25"
|
||||
|
|
@ -2222,12 +2064,6 @@ dependencies = [
|
|||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
|
||||
[[package]]
|
||||
name = "glow"
|
||||
version = "0.13.1"
|
||||
|
|
@ -2691,7 +2527,7 @@ dependencies = [
|
|||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.52.0",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2706,7 +2542,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#5306649be1cfb6c384da11e2ab25cafc4be79b14"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#8b37f658e2d0885b6972952cbf0e606d188ba2b7"
|
||||
dependencies = [
|
||||
"dnd",
|
||||
"iced_accessibility",
|
||||
|
|
@ -2724,7 +2560,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_accessibility"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#5306649be1cfb6c384da11e2ab25cafc4be79b14"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#8b37f658e2d0885b6972952cbf0e606d188ba2b7"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"accesskit_winit",
|
||||
|
|
@ -2733,7 +2569,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_core"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#5306649be1cfb6c384da11e2ab25cafc4be79b14"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#8b37f658e2d0885b6972952cbf0e606d188ba2b7"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"dnd",
|
||||
|
|
@ -2753,7 +2589,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_futures"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#5306649be1cfb6c384da11e2ab25cafc4be79b14"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#8b37f658e2d0885b6972952cbf0e606d188ba2b7"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"iced_core",
|
||||
|
|
@ -2766,7 +2602,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_graphics"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#5306649be1cfb6c384da11e2ab25cafc4be79b14"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#8b37f658e2d0885b6972952cbf0e606d188ba2b7"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"bytemuck",
|
||||
|
|
@ -2790,7 +2626,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_renderer"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#5306649be1cfb6c384da11e2ab25cafc4be79b14"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#8b37f658e2d0885b6972952cbf0e606d188ba2b7"
|
||||
dependencies = [
|
||||
"iced_graphics",
|
||||
"iced_tiny_skia",
|
||||
|
|
@ -2802,7 +2638,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_runtime"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#5306649be1cfb6c384da11e2ab25cafc4be79b14"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#8b37f658e2d0885b6972952cbf0e606d188ba2b7"
|
||||
dependencies = [
|
||||
"dnd",
|
||||
"iced_core",
|
||||
|
|
@ -2814,7 +2650,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_style"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#5306649be1cfb6c384da11e2ab25cafc4be79b14"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#8b37f658e2d0885b6972952cbf0e606d188ba2b7"
|
||||
dependencies = [
|
||||
"iced_core",
|
||||
"once_cell",
|
||||
|
|
@ -2824,7 +2660,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_tiny_skia"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#5306649be1cfb6c384da11e2ab25cafc4be79b14"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#8b37f658e2d0885b6972952cbf0e606d188ba2b7"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"cosmic-text",
|
||||
|
|
@ -2841,7 +2677,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_video_player"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/jackpot51/iced_video_player.git?branch=cosmic#edf6354fdd3d365df9177cd90de4a12024096dff"
|
||||
source = "git+https://github.com/jackpot51/iced_video_player.git?branch=cosmic#8086eb49d569f5174ae9750f45e7b2e566352d9d"
|
||||
dependencies = [
|
||||
"glib",
|
||||
"gstreamer",
|
||||
|
|
@ -2857,7 +2693,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_wgpu"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#5306649be1cfb6c384da11e2ab25cafc4be79b14"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#8b37f658e2d0885b6972952cbf0e606d188ba2b7"
|
||||
dependencies = [
|
||||
"as-raw-xcb-connection",
|
||||
"bitflags 2.6.0",
|
||||
|
|
@ -2886,7 +2722,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_widget"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#5306649be1cfb6c384da11e2ab25cafc4be79b14"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#8b37f658e2d0885b6972952cbf0e606d188ba2b7"
|
||||
dependencies = [
|
||||
"dnd",
|
||||
"iced_renderer",
|
||||
|
|
@ -2902,7 +2738,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "iced_winit"
|
||||
version = "0.12.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#5306649be1cfb6c384da11e2ab25cafc4be79b14"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#8b37f658e2d0885b6972952cbf0e606d188ba2b7"
|
||||
dependencies = [
|
||||
"dnd",
|
||||
"iced_graphics",
|
||||
|
|
@ -3192,24 +3028,12 @@ version = "1.5.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "lazycell"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "lebe"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
||||
|
||||
[[package]]
|
||||
name = "lexopt"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baff4b617f7df3d896f97fe922b64817f6cd9a756bb81d40f8883f2f66dcb401"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.159"
|
||||
|
|
@ -3219,10 +3043,10 @@ checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
|
|||
[[package]]
|
||||
name = "libcosmic"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#5306649be1cfb6c384da11e2ab25cafc4be79b14"
|
||||
source = "git+https://github.com/pop-os/libcosmic.git#8b37f658e2d0885b6972952cbf0e606d188ba2b7"
|
||||
dependencies = [
|
||||
"apply",
|
||||
"ashpd 0.9.1",
|
||||
"ashpd 0.9.2",
|
||||
"chrono",
|
||||
"cosmic-config",
|
||||
"cosmic-theme",
|
||||
|
|
@ -3411,15 +3235,6 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "malloc_buf"
|
||||
version = "0.0.6"
|
||||
|
|
@ -3494,12 +3309,6 @@ dependencies = [
|
|||
"smithay-clipboard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.4"
|
||||
|
|
@ -3630,16 +3439,6 @@ dependencies = [
|
|||
"memoffset 0.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "6.1.1"
|
||||
|
|
@ -3692,17 +3491,6 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.79",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
|
|
@ -3861,29 +3649,6 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oboe"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb"
|
||||
dependencies = [
|
||||
"jni",
|
||||
"ndk",
|
||||
"ndk-context",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"oboe-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oboe-sys"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.20.2"
|
||||
|
|
@ -4052,12 +3817,6 @@ version = "1.0.15"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "peeking_take_while"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
|
|
@ -5571,12 +5330,6 @@ dependencies = [
|
|||
"tiny-skia-path",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.0"
|
||||
|
|
@ -6024,17 +5777,7 @@ version = "0.52.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
|
||||
dependencies = [
|
||||
"windows-core 0.52.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.54.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
|
||||
dependencies = [
|
||||
"windows-core 0.54.0",
|
||||
"windows-core",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
|
|
@ -6047,16 +5790,6 @@ dependencies = [
|
|||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.54.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.48.0"
|
||||
|
|
@ -6079,15 +5812,6 @@ dependencies = [
|
|||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
|
|
|
|||
13
Cargo.toml
13
Cargo.toml
|
|
@ -5,13 +5,9 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
lazy_static = "1"
|
||||
paste = "1"
|
||||
serde = { version = "1", features = ["serde_derive"] }
|
||||
tokio = "1"
|
||||
url = "2"
|
||||
# ffmpeg
|
||||
cpal = { version = "0.15", optional = true }
|
||||
ffmpeg-next = { version = "6", optional = true }
|
||||
# Internationalization
|
||||
i18n-embed = { version = "0.13", features = ["fluent-system", "desktop-requester"] }
|
||||
i18n-embed-fl = "0.6"
|
||||
|
|
@ -19,12 +15,11 @@ rust-embed = "6"
|
|||
# Logging
|
||||
env_logger = "0.10"
|
||||
log = "0.4"
|
||||
lexopt = "0.3"
|
||||
|
||||
[dependencies.iced_video_player]
|
||||
git = "https://github.com/jackpot51/iced_video_player.git"
|
||||
branch = "cosmic"
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.libcosmic]
|
||||
git = "https://github.com/pop-os/libcosmic.git"
|
||||
|
|
@ -36,10 +31,8 @@ version = "0.2.1"
|
|||
features = ["serde"]
|
||||
|
||||
[features]
|
||||
default = ["gstreamer", "wgpu"]
|
||||
ffmpeg = ["dep:cpal", "dep:ffmpeg-next"]
|
||||
gstreamer = ["dep:iced_video_player", "wgpu"]
|
||||
wgpu = ["libcosmic/wgpu"]
|
||||
default = ["wgpu"]
|
||||
wgpu = ["iced_video_player/wgpu", "libcosmic/wgpu"]
|
||||
|
||||
[profile.release-with-debug]
|
||||
inherits = "release"
|
||||
|
|
|
|||
|
|
@ -4,12 +4,7 @@ use cosmic::{
|
|||
cosmic_config::{self, cosmic_config_derive::CosmicConfigEntry, CosmicConfigEntry},
|
||||
theme,
|
||||
};
|
||||
use lexopt::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{path::PathBuf, process};
|
||||
|
||||
#[cfg(feature = "ffmpeg")]
|
||||
use crate::app::hardware::DeviceType;
|
||||
|
||||
pub const CONFIG_VERSION: u64 = 1;
|
||||
|
||||
|
|
@ -34,68 +29,12 @@ impl AppTheme {
|
|||
#[serde(default)]
|
||||
pub struct Config {
|
||||
pub app_theme: AppTheme,
|
||||
#[cfg(feature = "ffmpeg")]
|
||||
pub hw_decoder: DeviceType,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
app_theme: AppTheme::System,
|
||||
#[cfg(feature = "ffmpeg")]
|
||||
hw_decoder: DeviceType::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffmpeg")]
|
||||
impl Config {
|
||||
pub fn with_args(&mut self, args: &mut Args) {
|
||||
if let Some(decoder) = args.decoder {
|
||||
self.hw_decoder = decoder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffmpeg")]
|
||||
pub struct Args {
|
||||
pub paths: Vec<PathBuf>,
|
||||
pub decoder: Option<DeviceType>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "ffmpeg")]
|
||||
impl Args {
|
||||
pub fn parse_args() -> Result<Self, lexopt::Error> {
|
||||
let mut paths = Vec::new();
|
||||
let mut decoder = None;
|
||||
|
||||
let mut parser = lexopt::Parser::from_env();
|
||||
while let Some(arg) = parser.next()? {
|
||||
match arg {
|
||||
Long("list-hwdec") => {
|
||||
println!("Supported hardware decoders:");
|
||||
for hwdec in DeviceType::supported_devices() {
|
||||
println!("\t* [{}] {hwdec}", hwdec.short_name());
|
||||
}
|
||||
process::exit(0);
|
||||
}
|
||||
Long("hwdec") => {
|
||||
decoder = Some(parser.value()?.parse()?);
|
||||
}
|
||||
Value(path) => {
|
||||
let path = path.parse()?;
|
||||
paths.push(path);
|
||||
}
|
||||
_ => return Err(arg.unexpected()),
|
||||
}
|
||||
}
|
||||
|
||||
if paths.is_empty() {
|
||||
return Err(lexopt::Error::MissingValue {
|
||||
option: Some("missing video path".into()),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self { paths, decoder })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
use ffmpeg_next::ffi::AVHWDeviceType;
|
||||
use serde::{
|
||||
de::{value::Error as DeError, Error as DeErrorTrait, Unexpected},
|
||||
Deserialize, Serialize,
|
||||
};
|
||||
|
||||
use super::iter::SupportedDeviceIter;
|
||||
|
||||
/// Delegate type for [`ffmpeg_next::ffi::AVHWDeviceType`] for configs.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DeviceType {
|
||||
None,
|
||||
/// Compute Unified Device Architecture
|
||||
/// Nvidia only.
|
||||
/// https://developer.nvidia.com/video-codec-sdk
|
||||
Cuda,
|
||||
/// Direct3D 11 Video API
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/medfound/direct3d-11-video-apis
|
||||
D3d11va,
|
||||
/// Direct3D 12 Video API
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/medfound/direct3d-12-video-overview
|
||||
D3d12va,
|
||||
/// DirectX Video Acceleration 2.0
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/medfound/about-dxva-2-0
|
||||
Dxva2,
|
||||
/// Direct Rendering Manager
|
||||
/// https://dri.freedesktop.org/wiki/DRM/
|
||||
Drm,
|
||||
/// MediaCodec
|
||||
/// Android only
|
||||
/// https://developer.android.com/reference/android/media/MediaCodec
|
||||
MediaCodec,
|
||||
/// OpenCL
|
||||
/// Only used in filters
|
||||
/// https://www.khronos.org/opencl/
|
||||
OpenCl,
|
||||
/// Intel Quick Sync Video
|
||||
/// https://www.intel.com/content/www/us/en/developer/tools/vpl/overview.html
|
||||
Qsv,
|
||||
/// Video Acceleration API
|
||||
/// https://www.intel.com/content/www/us/en/developer/articles/technical/linuxmedia-vaapi.html
|
||||
Vaapi,
|
||||
/// Video Decode and Presentation API for Unix
|
||||
/// https://www.freedesktop.org/wiki/Software/VDPAU/
|
||||
Vdpau,
|
||||
/// Video Toolbox
|
||||
/// https://developer.apple.com/documentation/videotoolbox
|
||||
VideoToolbox,
|
||||
/// Vulkan
|
||||
Vulkan,
|
||||
}
|
||||
|
||||
impl DeviceType {
|
||||
/// Hardware device names for user facing interfaces (logging, configs).
|
||||
pub const fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::None => "None",
|
||||
Self::Cuda => "CUDA",
|
||||
Self::Dxva2 => "DirectX Video Acceleration 2.0",
|
||||
Self::D3d11va => "DirectX 11 Video Acceleration",
|
||||
Self::D3d12va => "DirectX 12 Video Acceleration",
|
||||
Self::Drm => "Direct Rendering Manager (DRM)",
|
||||
Self::MediaCodec => "MediaCodec",
|
||||
Self::OpenCl => "OpenCL",
|
||||
Self::Qsv => "Intel Quick Video Sync",
|
||||
Self::Vaapi => "VA-API",
|
||||
Self::Vdpau => "VDPAU",
|
||||
Self::VideoToolbox => "VideoToolbox",
|
||||
Self::Vulkan => "Vulkan",
|
||||
}
|
||||
}
|
||||
|
||||
/// Short name for CLI arguments
|
||||
pub const fn short_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::None => "none",
|
||||
Self::Cuda => "cuda",
|
||||
Self::Dxva2 => "dxva2",
|
||||
Self::D3d11va => "d3d11va",
|
||||
Self::D3d12va => "d3d12va",
|
||||
Self::Drm => "drm",
|
||||
Self::MediaCodec => "mediacodec",
|
||||
Self::OpenCl => "opencl",
|
||||
Self::Qsv => "qsv",
|
||||
Self::Vaapi => "vaapi",
|
||||
Self::Vdpau => "vdpau",
|
||||
Self::VideoToolbox => "videotoolbox",
|
||||
Self::Vulkan => "vulkan",
|
||||
}
|
||||
}
|
||||
|
||||
/// System's supported hardware decoders
|
||||
pub fn supported_devices() -> SupportedDeviceIter {
|
||||
SupportedDeviceIter::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for DeviceType {
|
||||
type Err = DeError;
|
||||
|
||||
// av_hwdevice_find_type_by_name returns None for invalid device type names, but this type
|
||||
// is used for deserializing configs (etc.) so the error is preserved.
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"none" => Ok(Self::None),
|
||||
"cuda" => Ok(Self::Cuda),
|
||||
"dxva2" => Ok(Self::Dxva2),
|
||||
"d3d11va" => Ok(Self::D3d11va),
|
||||
"d3d12va" => Ok(Self::D3d12va),
|
||||
"drm" => Ok(Self::Drm),
|
||||
"mediacodec" => Ok(Self::MediaCodec),
|
||||
"opencl" => Ok(Self::OpenCl),
|
||||
"qsv" => Ok(Self::Qsv),
|
||||
"vaapi" => Ok(Self::Vaapi),
|
||||
"vdpau" => Ok(Self::Vdpau),
|
||||
"videotoolbox" => Ok(Self::VideoToolbox),
|
||||
"vulkan" => Ok(Self::Vulkan),
|
||||
_ => Err(DeError::invalid_value(
|
||||
Unexpected::Str(s),
|
||||
&"valid hardware decoder",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DeviceType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.name())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AVHWDeviceType> for DeviceType {
|
||||
fn from(value: AVHWDeviceType) -> Self {
|
||||
match value {
|
||||
AVHWDeviceType::AV_HWDEVICE_TYPE_NONE => Self::None,
|
||||
AVHWDeviceType::AV_HWDEVICE_TYPE_CUDA => Self::Cuda,
|
||||
AVHWDeviceType::AV_HWDEVICE_TYPE_DXVA2 => Self::Dxva2,
|
||||
AVHWDeviceType::AV_HWDEVICE_TYPE_D3D11VA => Self::D3d11va,
|
||||
// This variant exists in ffmpeg's C lib but not in Rust's crate yet.
|
||||
// AVHWDeviceType::AV_HWDEVICE_TYPE_D3D12VA => Self::D3d12va
|
||||
AVHWDeviceType::AV_HWDEVICE_TYPE_DRM => Self::Drm,
|
||||
AVHWDeviceType::AV_HWDEVICE_TYPE_MEDIACODEC => Self::MediaCodec,
|
||||
AVHWDeviceType::AV_HWDEVICE_TYPE_OPENCL => Self::OpenCl,
|
||||
AVHWDeviceType::AV_HWDEVICE_TYPE_QSV => Self::Qsv,
|
||||
AVHWDeviceType::AV_HWDEVICE_TYPE_VAAPI => Self::Vaapi,
|
||||
AVHWDeviceType::AV_HWDEVICE_TYPE_VDPAU => Self::Vdpau,
|
||||
AVHWDeviceType::AV_HWDEVICE_TYPE_VIDEOTOOLBOX => Self::VideoToolbox,
|
||||
AVHWDeviceType::AV_HWDEVICE_TYPE_VULKAN => Self::Vulkan,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DeviceType> for AVHWDeviceType {
|
||||
fn from(value: DeviceType) -> Self {
|
||||
match value {
|
||||
DeviceType::None => Self::AV_HWDEVICE_TYPE_NONE,
|
||||
DeviceType::Cuda => Self::AV_HWDEVICE_TYPE_CUDA,
|
||||
DeviceType::D3d11va => Self::AV_HWDEVICE_TYPE_D3D11VA,
|
||||
// NOTE: Next FFmpeg release
|
||||
DeviceType::D3d12va => Self::AV_HWDEVICE_TYPE_NONE,
|
||||
DeviceType::Dxva2 => Self::AV_HWDEVICE_TYPE_DXVA2,
|
||||
DeviceType::Drm => Self::AV_HWDEVICE_TYPE_DRM,
|
||||
DeviceType::MediaCodec => Self::AV_HWDEVICE_TYPE_MEDIACODEC,
|
||||
DeviceType::OpenCl => Self::AV_HWDEVICE_TYPE_OPENCL,
|
||||
DeviceType::Qsv => Self::AV_HWDEVICE_TYPE_QSV,
|
||||
DeviceType::Vaapi => Self::AV_HWDEVICE_TYPE_VAAPI,
|
||||
DeviceType::Vdpau => Self::AV_HWDEVICE_TYPE_VDPAU,
|
||||
DeviceType::VideoToolbox => Self::AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
|
||||
DeviceType::Vulkan => Self::AV_HWDEVICE_TYPE_VULKAN,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DeviceType {
|
||||
fn default() -> Self {
|
||||
Self::Vaapi
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
use ffmpeg_next::ffi::{av_hwdevice_iterate_types, AVHWDeviceType};
|
||||
|
||||
use super::device_type::DeviceType;
|
||||
|
||||
/// Iterator over system's supported hardware decoders.
|
||||
pub struct SupportedDeviceIter {
|
||||
current: AVHWDeviceType,
|
||||
}
|
||||
|
||||
impl Default for SupportedDeviceIter {
|
||||
fn default() -> Self {
|
||||
// SAFETY: FFmpeg's documentation states that the iterator is delimited by AV_HWDEVICE_TYPE_NONE.
|
||||
let current = unsafe { av_hwdevice_iterate_types(AVHWDeviceType::AV_HWDEVICE_TYPE_NONE) };
|
||||
Self { current }
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for SupportedDeviceIter {
|
||||
type Item = DeviceType;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
// None is a sentinel value that indicates the iterator is exhausted
|
||||
if self.current == AVHWDeviceType::AV_HWDEVICE_TYPE_NONE {
|
||||
None
|
||||
} else {
|
||||
let prev = self.current;
|
||||
// SAFETY: The docs and examples state that the iterator yields the next value
|
||||
// when the previous is passed in.
|
||||
self.current = unsafe { av_hwdevice_iterate_types(prev) };
|
||||
|
||||
Some(prev.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for SupportedDeviceIter {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::hint::black_box;
|
||||
|
||||
use super::*;
|
||||
|
||||
// The iterator's yielded values aren't important since hardware decoders vary by system
|
||||
// This is just a sanity check to ensure the iterator works
|
||||
#[test]
|
||||
fn supported_device_iter_doesnt_seg_fault() {
|
||||
for decoder in DeviceType::supported_devices() {
|
||||
black_box(decoder);
|
||||
}
|
||||
|
||||
let _decoders: Vec<_> = black_box(DeviceType::supported_devices().collect());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod device_type;
|
||||
pub mod iter;
|
||||
|
||||
pub use device_type::DeviceType;
|
||||
pub use iter::SupportedDeviceIter;
|
||||
|
|
@ -1,439 +0,0 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::{
|
||||
app::{message, Command, Core, Settings},
|
||||
cosmic_config::{self, CosmicConfigEntry},
|
||||
cosmic_theme, executor,
|
||||
iced::{
|
||||
event::{self, Event},
|
||||
keyboard::{Event as KeyEvent, Key, Modifiers},
|
||||
subscription::{self, Subscription},
|
||||
window, Alignment, Length, Limits,
|
||||
},
|
||||
widget, Application, ApplicationExt, Element,
|
||||
};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
collections::HashMap,
|
||||
env, process,
|
||||
sync::{mpsc, Arc, Mutex},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::{AppTheme, Args, Config, CONFIG_VERSION},
|
||||
fl,
|
||||
key_bind::{key_binds, KeyBind},
|
||||
localize,
|
||||
};
|
||||
|
||||
pub mod hardware;
|
||||
|
||||
mod player;
|
||||
use player::{PlayerMessage, VideoFrame, VideoQueue};
|
||||
|
||||
/// Runs application with these settings
|
||||
#[rustfmt::skip]
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
|
||||
|
||||
localize::localize();
|
||||
|
||||
let mut args = match Args::parse_args() {
|
||||
Ok(args) => args,
|
||||
Err(e) => {
|
||||
log::error!("{e}");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let (config_handler, config) = match cosmic_config::Config::new(App::APP_ID, CONFIG_VERSION) {
|
||||
Ok(config_handler) => {
|
||||
let mut config = match Config::get_entry(&config_handler) {
|
||||
Ok(ok) => ok,
|
||||
Err((errs, config)) => {
|
||||
log::info!("errors loading config: {:?}", errs);
|
||||
config
|
||||
}
|
||||
};
|
||||
// Update config with command line args
|
||||
config.with_args(&mut args);
|
||||
(Some(config_handler), config)
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("failed to create config handler: {}", err);
|
||||
(None, Config::default())
|
||||
}
|
||||
};
|
||||
|
||||
//TODO: support using multiple paths
|
||||
let Args { mut paths, .. } = args;
|
||||
let path = paths.pop().unwrap();
|
||||
|
||||
// TODO: Update video player config when it's updated via the app
|
||||
let (player_tx, video_queue_lock) = player::run(path, config.clone());
|
||||
|
||||
let mut settings = Settings::default();
|
||||
settings = settings.theme(config.app_theme.theme());
|
||||
settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0));
|
||||
|
||||
let flags = Flags {
|
||||
config_handler,
|
||||
config,
|
||||
player_tx,
|
||||
video_queue_lock,
|
||||
};
|
||||
cosmic::app::run::<App>(settings, flags)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Action {
|
||||
Todo,
|
||||
SeekBackward,
|
||||
SeekForward,
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub fn message(&self) -> Message {
|
||||
match self {
|
||||
Self::Todo => Message::Todo,
|
||||
Self::SeekBackward => Message::Player(PlayerMessage::SeekRelative(-10.0)),
|
||||
Self::SeekForward => Message::Player(PlayerMessage::SeekRelative(10.0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Flags {
|
||||
config_handler: Option<cosmic_config::Config>,
|
||||
config: Config,
|
||||
player_tx: mpsc::Sender<PlayerMessage>,
|
||||
video_queue_lock: Arc<Mutex<VideoQueue>>,
|
||||
}
|
||||
|
||||
/// Messages that are used specifically by our [`App`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
Todo,
|
||||
AppTheme(AppTheme),
|
||||
Config(Config),
|
||||
Key(Modifiers, Key),
|
||||
Player(PlayerMessage),
|
||||
SystemThemeModeChange(cosmic_theme::ThemeMode),
|
||||
Tick(Instant),
|
||||
ToggleContextPage(ContextPage),
|
||||
WindowClose,
|
||||
WindowNew,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ContextPage {
|
||||
Settings,
|
||||
}
|
||||
|
||||
impl ContextPage {
|
||||
fn title(&self) -> String {
|
||||
match self {
|
||||
Self::Settings => fl!("settings"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`App`] stores application-specific state.
|
||||
pub struct App {
|
||||
core: Core,
|
||||
flags: Flags,
|
||||
app_themes: Vec<String>,
|
||||
context_page: ContextPage,
|
||||
key_binds: HashMap<KeyBind, Action>,
|
||||
handle_opt: Option<widget::image::Handle>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn update_config(&mut self) -> Command<Message> {
|
||||
cosmic::app::command::set_theme(self.flags.config.app_theme.theme())
|
||||
}
|
||||
|
||||
fn update_title(&mut self) -> Command<Message> {
|
||||
let title = "COSMIC Media Player";
|
||||
self.set_header_title(title.to_string());
|
||||
self.set_window_title(title.to_string())
|
||||
}
|
||||
|
||||
fn settings(&self) -> Element<Message> {
|
||||
let app_theme_selected = match self.flags.config.app_theme {
|
||||
AppTheme::Dark => 1,
|
||||
AppTheme::Light => 2,
|
||||
AppTheme::System => 0,
|
||||
};
|
||||
widget::settings::view_column(vec![widget::settings::view_section(fl!("appearance"))
|
||||
.add(
|
||||
widget::settings::item::builder(fl!("theme")).control(widget::dropdown(
|
||||
&self.app_themes,
|
||||
Some(app_theme_selected),
|
||||
move |index| {
|
||||
Message::AppTheme(match index {
|
||||
1 => AppTheme::Dark,
|
||||
2 => AppTheme::Light,
|
||||
_ => AppTheme::System,
|
||||
})
|
||||
},
|
||||
)),
|
||||
)
|
||||
.into()])
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement [`Application`] to integrate with COSMIC.
|
||||
impl Application for App {
|
||||
/// Default async executor to use with the app.
|
||||
type Executor = executor::Default;
|
||||
|
||||
/// Argument received
|
||||
type Flags = Flags;
|
||||
|
||||
/// Message type specific to our [`App`].
|
||||
type Message = Message;
|
||||
|
||||
/// The unique application ID to supply to the window manager.
|
||||
const APP_ID: &'static str = "com.system76.CosmicPlayer";
|
||||
|
||||
fn core(&self) -> &Core {
|
||||
&self.core
|
||||
}
|
||||
|
||||
fn core_mut(&mut self) -> &mut Core {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
/// Creates the application, and optionally emits command on initialize.
|
||||
fn init(core: Core, flags: Self::Flags) -> (Self, Command<Self::Message>) {
|
||||
let app_themes = vec![fl!("match-desktop"), fl!("dark"), fl!("light")];
|
||||
let mut app = App {
|
||||
core,
|
||||
flags,
|
||||
app_themes,
|
||||
context_page: ContextPage::Settings,
|
||||
key_binds: key_binds(),
|
||||
handle_opt: None,
|
||||
};
|
||||
|
||||
let command = app.update_title();
|
||||
(app, command)
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> Command<Message> {
|
||||
if self.core.window.show_context {
|
||||
// Close context drawer if open
|
||||
self.core.window.show_context = false;
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
|
||||
/// Handle application events here.
|
||||
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
|
||||
// Helper for updating config values efficiently
|
||||
macro_rules! config_set {
|
||||
($name: ident, $value: expr) => {
|
||||
match &self.flags.config_handler {
|
||||
Some(config_handler) => {
|
||||
match paste::paste! { self.flags.config.[<set_ $name>](config_handler, $value) } {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"failed to save config {:?}: {}",
|
||||
stringify!($name),
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.flags.config.$name = $value;
|
||||
log::warn!(
|
||||
"failed to save config {:?}: no config handler",
|
||||
stringify!($name)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
match message {
|
||||
Message::Todo => {
|
||||
log::warn!("TODO");
|
||||
}
|
||||
Message::AppTheme(app_theme) => {
|
||||
config_set!(app_theme, app_theme);
|
||||
return self.update_config();
|
||||
}
|
||||
Message::Config(config) => {
|
||||
if config != self.flags.config {
|
||||
log::info!("update config");
|
||||
//TODO: update syntax theme by clearing tabs, only if needed
|
||||
self.flags.config = config;
|
||||
return self.update_config();
|
||||
}
|
||||
}
|
||||
Message::Key(modifiers, key) => {
|
||||
for (key_bind, action) in self.key_binds.iter() {
|
||||
if key_bind.matches(modifiers, &key) {
|
||||
return self.update(action.message());
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Player(player_message) => {
|
||||
self.flags.player_tx.send(player_message).unwrap();
|
||||
}
|
||||
Message::SystemThemeModeChange(_theme_mode) => {
|
||||
return self.update_config();
|
||||
}
|
||||
Message::Tick(frame_time) => {
|
||||
let start = Instant::now();
|
||||
|
||||
let mut video_frame_opt: Option<VideoFrame> = None;
|
||||
let delayed_time = {
|
||||
let mut video_queue = self.flags.video_queue_lock.lock().unwrap();
|
||||
let delayed_time = frame_time - video_queue.delay;
|
||||
while let Some(video_frame) = video_queue.data.pop_front() {
|
||||
if video_frame.1.unwrap_or(delayed_time) <= delayed_time {
|
||||
if let Some(old_frame) = video_frame_opt {
|
||||
//TODO: log this outside of locking video_queue_lock?
|
||||
log::warn!("skipping video frame {:?}", old_frame.0.pts());
|
||||
}
|
||||
// Frame is ready to be shown
|
||||
video_frame_opt = Some(video_frame);
|
||||
} else {
|
||||
// Put frame back and exit loop
|
||||
video_queue.data.push_front(video_frame);
|
||||
break;
|
||||
}
|
||||
}
|
||||
delayed_time
|
||||
};
|
||||
|
||||
match video_frame_opt {
|
||||
Some(video_frame) => {
|
||||
let pts = video_frame.0.pts();
|
||||
let present_time_opt = video_frame.1;
|
||||
self.handle_opt = Some(video_frame.into_handle());
|
||||
|
||||
let duration = start.elapsed();
|
||||
log::debug!(
|
||||
"converted video frame at {:?} to handle in {:?}",
|
||||
pts,
|
||||
duration
|
||||
);
|
||||
|
||||
if let Some(present_time) = present_time_opt {
|
||||
if present_time > delayed_time {
|
||||
let ahead = present_time - delayed_time;
|
||||
if ahead > Duration::from_millis(1) {
|
||||
log::debug!("video ahead {:?}", ahead);
|
||||
}
|
||||
} else {
|
||||
let behind = delayed_time - present_time;
|
||||
if behind > Duration::from_millis(1) {
|
||||
log::debug!("video behind {:?}", behind);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
Message::ToggleContextPage(context_page) => {
|
||||
//TODO: ensure context menus are closed
|
||||
if self.context_page == context_page {
|
||||
self.core.window.show_context = !self.core.window.show_context;
|
||||
} else {
|
||||
self.context_page = context_page;
|
||||
self.core.window.show_context = true;
|
||||
}
|
||||
self.set_context_title(context_page.title());
|
||||
}
|
||||
Message::WindowClose => {
|
||||
return window::close(window::Id::MAIN);
|
||||
}
|
||||
Message::WindowNew => match env::current_exe() {
|
||||
Ok(exe) => match process::Command::new(&exe).spawn() {
|
||||
Ok(_child) => {}
|
||||
Err(err) => {
|
||||
log::error!("failed to execute {:?}: {}", exe, err);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
log::error!("failed to get current executable path: {}", err);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn context_drawer(&self) -> Option<Element<Message>> {
|
||||
if !self.core.window.show_context {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(match self.context_page {
|
||||
ContextPage::Settings => self.settings(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a view after each update.
|
||||
fn view(&self) -> Element<Self::Message> {
|
||||
let content: Element<_> = match &self.handle_opt {
|
||||
Some(handle) => widget::image(handle.clone())
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into(),
|
||||
None => widget::text("Loading").into(),
|
||||
};
|
||||
|
||||
// Uncomment to debug layout:
|
||||
//content.explain(cosmic::iced::Color::WHITE)
|
||||
content
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Self::Message> {
|
||||
struct ConfigSubscription;
|
||||
struct ThemeSubscription;
|
||||
|
||||
Subscription::batch([
|
||||
window::frames().map(|(_window_id, instant)| Message::Tick(instant)),
|
||||
event::listen_with(|event, _status| match event {
|
||||
Event::Keyboard(KeyEvent::KeyPressed { key, modifiers, .. }) => {
|
||||
Some(Message::Key(modifiers, key))
|
||||
}
|
||||
_ => None,
|
||||
}),
|
||||
cosmic_config::config_subscription(
|
||||
TypeId::of::<ConfigSubscription>(),
|
||||
Self::APP_ID.into(),
|
||||
CONFIG_VERSION,
|
||||
)
|
||||
.map(|update| {
|
||||
if !update.errors.is_empty() {
|
||||
log::debug!("errors loading config: {:?}", update.errors);
|
||||
}
|
||||
Message::SystemThemeModeChange(update.config)
|
||||
}),
|
||||
cosmic_config::config_subscription::<_, cosmic_theme::ThemeMode>(
|
||||
TypeId::of::<ThemeSubscription>(),
|
||||
cosmic_theme::THEME_MODE_ID.into(),
|
||||
cosmic_theme::ThemeMode::version(),
|
||||
)
|
||||
.map(|update| {
|
||||
if !update.errors.is_empty() {
|
||||
log::debug!("errors loading theme mode: {:?}", update.errors);
|
||||
}
|
||||
Message::SystemThemeModeChange(update.config)
|
||||
}),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
@ -1,654 +0,0 @@
|
|||
extern crate ffmpeg_next as ffmpeg;
|
||||
|
||||
use cosmic::widget;
|
||||
use cpal::{
|
||||
traits::{DeviceTrait, HostTrait, StreamTrait},
|
||||
FromSample, SizedSample,
|
||||
};
|
||||
use ffmpeg::{
|
||||
codec, ffi,
|
||||
format::{input, Pixel},
|
||||
media::Type,
|
||||
software::{resampling, scaling},
|
||||
util::{
|
||||
channel_layout, error,
|
||||
format::sample,
|
||||
frame::{audio::Audio, video::Video},
|
||||
},
|
||||
Packet,
|
||||
};
|
||||
use std::{
|
||||
cmp,
|
||||
collections::VecDeque,
|
||||
error::Error,
|
||||
path::{Path, PathBuf},
|
||||
ptr, slice,
|
||||
sync::{mpsc, Arc, Mutex},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
//TODO: calculate presentation time of end of queue
|
||||
pub struct AudioQueue {
|
||||
pub channels: usize,
|
||||
pub rate: f64,
|
||||
pub data: VecDeque<f32>,
|
||||
// Delay for data to hit speakers, used to sync with video
|
||||
pub delay: Duration,
|
||||
}
|
||||
|
||||
impl AudioQueue {
|
||||
pub fn new(channels: cpal::ChannelCount, rate: cpal::SampleRate) -> Self {
|
||||
Self {
|
||||
channels: channels as usize,
|
||||
rate: rate.0 as f64,
|
||||
data: VecDeque::new(),
|
||||
delay: Duration::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn duration(&self) -> Duration {
|
||||
self.duration_for_samples(self.data.len())
|
||||
}
|
||||
|
||||
pub fn duration_for_samples(&self, samples: usize) -> Duration {
|
||||
let frames = samples / self.channels;
|
||||
let seconds = (frames as f64) / self.rate;
|
||||
Duration::from_secs_f64(seconds)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PlayerMessage {
|
||||
SeekRelative(f64),
|
||||
}
|
||||
|
||||
pub struct VideoFrame(pub Video, pub Option<Instant>);
|
||||
|
||||
impl VideoFrame {
|
||||
pub fn into_handle(self) -> widget::image::Handle {
|
||||
let width = self.0.width();
|
||||
let height = self.0.height();
|
||||
widget::image::Handle::from_pixels(width, height, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for VideoFrame {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.data(0)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VideoQueue {
|
||||
pub data: VecDeque<VideoFrame>,
|
||||
// Delay to add to each frame to sync with audio
|
||||
pub delay: Duration,
|
||||
}
|
||||
|
||||
impl VideoQueue {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
data: VecDeque::new(),
|
||||
delay: Duration::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, frame: VideoFrame) {
|
||||
// Discard all frames that are newer than frame to fix seeking and duration calculation
|
||||
self.data
|
||||
.retain(|other| other.1.map_or(true, |x| x <= frame.1.unwrap_or(x)));
|
||||
self.data.push_back(frame);
|
||||
}
|
||||
|
||||
pub fn duration(&self) -> Duration {
|
||||
//TODO: can accurate duration actually be calculated since one frame would count as zero?
|
||||
let mut start_end_opt = None;
|
||||
for frame in self.data.iter() {
|
||||
if let Some(frame_time) = frame.1 {
|
||||
start_end_opt = Some(match start_end_opt {
|
||||
Some((start, end)) => (cmp::min(start, frame_time), cmp::max(end, frame_time)),
|
||||
None => (frame_time, frame_time),
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some((start, end)) = start_end_opt {
|
||||
end.duration_since(start)
|
||||
} else {
|
||||
Duration::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cpal() -> (
|
||||
cpal::SupportedStreamConfig,
|
||||
Box<dyn StreamTrait>,
|
||||
Arc<Mutex<AudioQueue>>,
|
||||
) {
|
||||
let host = cpal::default_host();
|
||||
let device = host
|
||||
.default_output_device()
|
||||
.expect("failed to get default audio output device");
|
||||
let config = device
|
||||
.default_output_config()
|
||||
.expect("failed to get default audio output config");
|
||||
println!("{:?}: {:?}", device.name(), config);
|
||||
|
||||
let audio_queue_lock = Arc::new(Mutex::new(AudioQueue::new(
|
||||
config.channels(),
|
||||
config.sample_rate(),
|
||||
)));
|
||||
let stream = {
|
||||
let config = config.clone();
|
||||
let audio_queue_lock = audio_queue_lock.clone();
|
||||
match config.sample_format() {
|
||||
cpal::SampleFormat::I8 => cpal_stream::<i8>(device, config.into(), audio_queue_lock),
|
||||
cpal::SampleFormat::I16 => cpal_stream::<i16>(device, config.into(), audio_queue_lock),
|
||||
// cpal::SampleFormat::I24 => cpal_stream::<I24>(device, config.into(), audio_queue_lock),
|
||||
cpal::SampleFormat::I32 => cpal_stream::<i32>(device, config.into(), audio_queue_lock),
|
||||
// cpal::SampleFormat::I48 => cpal_stream::<I48>(device, config.into(), audio_queue_lock),
|
||||
cpal::SampleFormat::I64 => cpal_stream::<i64>(device, config.into(), audio_queue_lock),
|
||||
cpal::SampleFormat::U8 => cpal_stream::<u8>(device, config.into(), audio_queue_lock),
|
||||
cpal::SampleFormat::U16 => cpal_stream::<u16>(device, config.into(), audio_queue_lock),
|
||||
// cpal::SampleFormat::U24 => cpal_stream::<U24>(device, config.into(), audio_queue_lock),
|
||||
cpal::SampleFormat::U32 => cpal_stream::<u32>(device, config.into(), audio_queue_lock),
|
||||
// cpal::SampleFormat::U48 => cpal_stream::<U48>(device, config.into(), audio_queue_lock),
|
||||
cpal::SampleFormat::U64 => cpal_stream::<u64>(device, config.into(), audio_queue_lock),
|
||||
cpal::SampleFormat::F32 => cpal_stream::<f32>(device, config.into(), audio_queue_lock),
|
||||
cpal::SampleFormat::F64 => cpal_stream::<f64>(device, config.into(), audio_queue_lock),
|
||||
sample_format => panic!("unsupported sample format '{sample_format}'"),
|
||||
}
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
(config, stream, audio_queue_lock)
|
||||
}
|
||||
|
||||
fn cpal_stream<T>(
|
||||
device: cpal::Device,
|
||||
config: cpal::StreamConfig,
|
||||
audio_queue_lock: Arc<Mutex<AudioQueue>>,
|
||||
) -> Result<Box<dyn StreamTrait>, Box<dyn Error>>
|
||||
where
|
||||
T: SizedSample + FromSample<f32>,
|
||||
{
|
||||
let data_fn = {
|
||||
move |samples: &mut [T], info: &cpal::OutputCallbackInfo| {
|
||||
let timestamp = info.timestamp();
|
||||
let delay = timestamp.playback.duration_since(×tamp.callback);
|
||||
|
||||
let mut underrun = 0;
|
||||
{
|
||||
//TODO: buffer audio
|
||||
let mut audio_queue = audio_queue_lock.lock().unwrap();
|
||||
//TODO: also add samples time?
|
||||
audio_queue.delay = delay.unwrap_or_default();
|
||||
for sample in samples {
|
||||
let float = match audio_queue.data.pop_front() {
|
||||
Some(some) => some,
|
||||
None => {
|
||||
underrun += 1;
|
||||
0.0
|
||||
}
|
||||
};
|
||||
*sample = T::from_sample(float);
|
||||
}
|
||||
};
|
||||
if underrun > 0 {
|
||||
log::error!("audio underrun {}", underrun);
|
||||
}
|
||||
}
|
||||
};
|
||||
let err_fn = |err| eprintln!("an error occurred on stream: {}", err);
|
||||
let stream = device.build_output_stream(&config, data_fn, err_fn, None)?;
|
||||
Ok(Box::new(stream))
|
||||
}
|
||||
|
||||
fn ffmpeg_thread<P: AsRef<Path>>(
|
||||
path: P,
|
||||
player_rx: mpsc::Receiver<PlayerMessage>,
|
||||
video_queue_lock: Arc<Mutex<VideoQueue>>,
|
||||
config: Config,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let (audio_config, cpal_stream, audio_queue_lock) = cpal();
|
||||
|
||||
let mut ictx = input(&path)?;
|
||||
|
||||
let video_stream = ictx
|
||||
.streams()
|
||||
.best(Type::Video)
|
||||
.ok_or(ffmpeg::Error::StreamNotFound)?;
|
||||
let video_stream_index = video_stream.index();
|
||||
let video_time_base = f64::from(video_stream.time_base());
|
||||
|
||||
let mut video_decoder = {
|
||||
let mut video_decoder_context =
|
||||
codec::context::Context::from_parameters(video_stream.parameters())?;
|
||||
|
||||
//TODO: safe wrappers
|
||||
let mut hw_device_ctx = ptr::null_mut();
|
||||
unsafe {
|
||||
//TODO: support other types
|
||||
let hw_device_kind = config.hw_decoder;
|
||||
if ffi::av_hwdevice_ctx_create(
|
||||
&mut hw_device_ctx,
|
||||
hw_device_kind.into(),
|
||||
ptr::null(),
|
||||
ptr::null_mut(),
|
||||
0,
|
||||
) == 0
|
||||
{
|
||||
log::info!("using {hw_device_kind} decoding");
|
||||
(&mut *video_decoder_context.as_mut_ptr()).hw_device_ctx =
|
||||
ffi::av_buffer_ref(hw_device_ctx);
|
||||
} else {
|
||||
//TODO: support other hardware devices
|
||||
log::warn!(
|
||||
"failed to use {hw_device_kind} decoding, falling back to software decoding"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
video_decoder_context.decoder().video()?
|
||||
};
|
||||
|
||||
let (cpu_frame_tx, cpu_frame_rx) = mpsc::channel::<(Video, Option<Instant>)>();
|
||||
{
|
||||
let video_format = video_decoder.format();
|
||||
let video_width = video_decoder.width();
|
||||
let video_height = video_decoder.height();
|
||||
let video_queue_lock = video_queue_lock.clone();
|
||||
thread::Builder::new()
|
||||
.name("video_scale".to_string())
|
||||
.spawn(move || {
|
||||
let mut video_scaler = scaling::context::Context::get(
|
||||
video_format,
|
||||
video_width,
|
||||
video_height,
|
||||
Pixel::RGBA,
|
||||
video_width,
|
||||
video_height,
|
||||
scaling::Flags::FAST_BILINEAR,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
let mut recv_opt: Option<(Video, Option<Instant>)> = None;
|
||||
/*TODO: SKIP
|
||||
while let Ok(recv) = cpu_frame_rx.try_recv() {
|
||||
if let Some((old_frame, _)) = recv_opt {
|
||||
//TODO: only skip if behind (frames come in weird timing from codecs)
|
||||
log::warn!("skipping cpu video frame at {:?}", old_frame.pts());
|
||||
}
|
||||
recv_opt = Some(recv);
|
||||
}
|
||||
*/
|
||||
let (cpu_frame, sync_time_opt) = match recv_opt {
|
||||
Some(some) => some,
|
||||
None => cpu_frame_rx.recv().unwrap(),
|
||||
};
|
||||
let pts_opt = cpu_frame.pts();
|
||||
|
||||
// Start count after blocking recv
|
||||
let start = Instant::now();
|
||||
|
||||
video_scaler.cached(
|
||||
cpu_frame.format(),
|
||||
cpu_frame.width(),
|
||||
cpu_frame.height(),
|
||||
Pixel::RGBA,
|
||||
cpu_frame.width(),
|
||||
cpu_frame.height(),
|
||||
scaling::Flags::FAST_BILINEAR,
|
||||
);
|
||||
|
||||
let mut scaled_frame = Video::empty();
|
||||
video_scaler.run(&cpu_frame, &mut scaled_frame).unwrap();
|
||||
scaled_frame.set_pts(pts_opt);
|
||||
|
||||
let present_time_opt = if let Some(pts) = pts_opt {
|
||||
let expected_float = pts as f64 * video_time_base;
|
||||
let expected = Duration::from_secs_f64(expected_float);
|
||||
if let Some(sync_time) = sync_time_opt {
|
||||
Some(sync_time + expected)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let video_frame = VideoFrame(scaled_frame, present_time_opt);
|
||||
{
|
||||
let mut video_queue = video_queue_lock.lock().unwrap();
|
||||
video_queue.push(video_frame);
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
log::debug!("scaled video frame at {:?} in {:?}", pts_opt, duration,);
|
||||
}
|
||||
})?
|
||||
};
|
||||
|
||||
// Sync channel to prevent allocation issues and falling behind
|
||||
let (gpu_frame_tx, gpu_frame_rx) = mpsc::sync_channel::<(Video, Option<Instant>)>(2);
|
||||
thread::Builder::new()
|
||||
.name("video_map_gpu_cpu".to_string())
|
||||
.spawn(move || {
|
||||
loop {
|
||||
let mut recv_opt: Option<(Video, Option<Instant>)> = None;
|
||||
/*TODO: SKIP
|
||||
while let Ok(recv) = gpu_frame_rx.try_recv() {
|
||||
if let Some((old_frame, _)) = recv_opt {
|
||||
//TODO: only skip if behind (frames come in weird timing from codecs)
|
||||
log::warn!("skipping gpu video frame at {:?}", old_frame.pts());
|
||||
}
|
||||
recv_opt = Some(recv);
|
||||
}
|
||||
*/
|
||||
let (gpu_frame, sync_time_opt) = match recv_opt {
|
||||
Some(some) => some,
|
||||
None => gpu_frame_rx.recv().unwrap(),
|
||||
};
|
||||
let pts = gpu_frame.pts();
|
||||
|
||||
// Start timer after blocking recv
|
||||
let start = Instant::now();
|
||||
|
||||
let mut cpu_frame = Video::empty();
|
||||
unsafe {
|
||||
if (&*gpu_frame.as_ptr()).hw_frames_ctx.is_null() {
|
||||
cpu_frame = gpu_frame;
|
||||
} else {
|
||||
if ffi::av_hwframe_transfer_data(
|
||||
cpu_frame.as_mut_ptr(),
|
||||
gpu_frame.as_ptr(),
|
||||
0,
|
||||
) < 0
|
||||
{
|
||||
panic!("av_hwframe_transfer_data failed");
|
||||
}
|
||||
/*TODO: MAP OR TRANSFER?
|
||||
if ffi::av_hwframe_map(
|
||||
cpu_frame.as_mut_ptr(),
|
||||
gpu_frame.as_ptr(),
|
||||
ffi::AV_HWFRAME_MAP_READ as i32,
|
||||
) < 0
|
||||
{
|
||||
panic!("av_hwframe_map failed");
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
cpu_frame.set_pts(pts);
|
||||
cpu_frame_tx.send((cpu_frame, sync_time_opt)).unwrap();
|
||||
|
||||
let duration = start.elapsed();
|
||||
log::debug!("map gpu video frame to cpu at {:?} in {:?}", pts, duration);
|
||||
}
|
||||
})?;
|
||||
|
||||
// Sync channel to prevent getting too far behind
|
||||
let (video_packet_tx, video_packet_rx) = mpsc::sync_channel::<(Packet, Option<Instant>)>(2);
|
||||
thread::Builder::new()
|
||||
.name("video_decode".to_string())
|
||||
.spawn(move || {
|
||||
let mut eof = false;
|
||||
while !eof {
|
||||
let mut sync_time_opt = None;
|
||||
|
||||
{
|
||||
let packet_res = video_packet_rx.recv();
|
||||
|
||||
// Start timer after blocking recv
|
||||
let start = Instant::now();
|
||||
|
||||
let mut packet_pts = None;
|
||||
match packet_res {
|
||||
Ok((packet, time_opt)) => {
|
||||
packet_pts = packet.pts();
|
||||
sync_time_opt = time_opt;
|
||||
video_decoder.send_packet(&packet).unwrap();
|
||||
}
|
||||
Err(_err) => {
|
||||
video_decoder.send_eof().unwrap();
|
||||
eof = true;
|
||||
}
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
log::debug!("sent packet at {:?} in {:?}", packet_pts, duration);
|
||||
}
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
let mut pts = None;
|
||||
let mut video_frames = 0;
|
||||
loop {
|
||||
let mut gpu_frame = Video::empty();
|
||||
if video_decoder.receive_frame(&mut gpu_frame).is_ok() {
|
||||
pts = gpu_frame.pts();
|
||||
gpu_frame_tx.send((gpu_frame, sync_time_opt)).unwrap();
|
||||
video_frames += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if video_frames > 0 {
|
||||
let duration = start.elapsed();
|
||||
log::debug!(
|
||||
"received {} video frames at {:?} in {:?}",
|
||||
video_frames,
|
||||
pts,
|
||||
duration
|
||||
);
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
let audio_stream = ictx
|
||||
.streams()
|
||||
.best(Type::Audio)
|
||||
.ok_or(ffmpeg::Error::StreamNotFound)?;
|
||||
let audio_stream_index = audio_stream.index();
|
||||
let audio_time_base = f64::from(audio_stream.time_base());
|
||||
|
||||
let audio_context_decoder =
|
||||
codec::context::Context::from_parameters(audio_stream.parameters())?;
|
||||
let mut audio_decoder = audio_context_decoder.decoder().audio()?;
|
||||
|
||||
let mut audio_resampler = resampling::Context::get(
|
||||
audio_decoder.format(),
|
||||
audio_decoder.channel_layout(),
|
||||
audio_decoder.rate(),
|
||||
//TODO: support other formats?
|
||||
sample::Sample::F32(sample::Type::Packed),
|
||||
match audio_config.channels() {
|
||||
1 => channel_layout::ChannelLayout::MONO,
|
||||
2 => channel_layout::ChannelLayout::STEREO,
|
||||
//TODO: more channel configs
|
||||
unsupported => {
|
||||
panic!("unsupported audio channels {:?}", unsupported);
|
||||
}
|
||||
},
|
||||
audio_config.sample_rate().0,
|
||||
)?;
|
||||
|
||||
let min_sleep = Duration::from_millis(1);
|
||||
let min_skip = Duration::from_millis(1);
|
||||
let mut receive_and_process_decoded_audio_frames = |decoder: &mut ffmpeg::decoder::Audio,
|
||||
sync_time_opt: &mut Option<Instant>|
|
||||
-> Result<(), ffmpeg::Error> {
|
||||
let mut decoded = Audio::empty();
|
||||
let mut resampled = Audio::empty();
|
||||
let mut pts_opt = None;
|
||||
while decoder.receive_frame(&mut decoded).is_ok() {
|
||||
pts_opt = decoded.pts();
|
||||
|
||||
audio_resampler.run(&decoded, &mut resampled)?;
|
||||
{
|
||||
// plane method doesn't work with packed samples, so do it manually
|
||||
let plane = unsafe {
|
||||
slice::from_raw_parts(
|
||||
(*resampled.as_ptr()).data[0] as *const f32,
|
||||
resampled.samples() * resampled.channels() as usize,
|
||||
)
|
||||
};
|
||||
{
|
||||
let mut audio_queue = audio_queue_lock.lock().unwrap();
|
||||
audio_queue.data.extend(plane);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(pts) = pts_opt {
|
||||
let expected_float = pts as f64 * audio_time_base;
|
||||
let expected = Duration::from_secs_f64(expected_float);
|
||||
if let Some(sync_time) = &sync_time_opt {
|
||||
// Sync with audio
|
||||
let actual = sync_time.elapsed();
|
||||
if expected > actual {
|
||||
let sleep = expected - actual;
|
||||
if sleep > min_sleep {
|
||||
// We leave min_sleep of buffer room
|
||||
log::debug!("audio ahead {:?}", sleep);
|
||||
}
|
||||
} else {
|
||||
let skip = actual - expected;
|
||||
if skip > min_skip {
|
||||
//TODO: handle frame skipping
|
||||
log::debug!("audio behind {:?}", skip);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Set up sync
|
||||
*sync_time_opt = Some(Instant::now() - expected);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
//TODO: dynamically choose this
|
||||
let buffer_duration = Duration::from_millis(250);
|
||||
|
||||
// Start CPAL stream
|
||||
cpal_stream.play()?;
|
||||
|
||||
let mut sync_time_opt = None;
|
||||
let mut seconds_opt = None;
|
||||
loop {
|
||||
let mut packet = Packet::empty();
|
||||
match packet.read(&mut ictx) {
|
||||
Ok(()) => {
|
||||
if packet.stream() == video_stream_index {
|
||||
video_packet_tx.send((packet, sync_time_opt)).unwrap();
|
||||
} else if packet.stream() == audio_stream_index {
|
||||
audio_decoder.send_packet(&packet)?;
|
||||
receive_and_process_decoded_audio_frames(
|
||||
&mut audio_decoder,
|
||||
&mut sync_time_opt,
|
||||
)?;
|
||||
if let Some(pts) = packet.pts() {
|
||||
seconds_opt = Some(pts as f64 * audio_time_base);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error::Error::Eof) => break,
|
||||
Err(_err) => {}
|
||||
}
|
||||
|
||||
let (audio_queue_duration, audio_queue_delay) = {
|
||||
let audio_queue = audio_queue_lock.lock().unwrap();
|
||||
(audio_queue.duration(), audio_queue.delay)
|
||||
};
|
||||
|
||||
let (video_queue_duration, video_queue_delay) = {
|
||||
let mut video_queue = video_queue_lock.lock().unwrap();
|
||||
let video_queue_duration = video_queue.duration();
|
||||
if video_queue_duration < buffer_duration {
|
||||
// If we do not have enough video queued, delay the video output
|
||||
video_queue.delay = buffer_duration - video_queue_duration;
|
||||
} else {
|
||||
video_queue.delay = Duration::default();
|
||||
}
|
||||
// Add audio queue delay to sync with audio
|
||||
video_queue.delay += audio_queue_delay;
|
||||
(video_queue_duration, video_queue.delay)
|
||||
};
|
||||
|
||||
log::debug!(
|
||||
"video: {:?}, {:?} audio: {:?}, {:?}",
|
||||
video_queue_duration,
|
||||
video_queue_delay,
|
||||
audio_queue_duration,
|
||||
audio_queue_delay
|
||||
);
|
||||
|
||||
let min_queue_duration = cmp::min(video_queue_duration, audio_queue_duration);
|
||||
if min_queue_duration > buffer_duration {
|
||||
// If we have enough queued, we can sleep
|
||||
let sleep = min_queue_duration - buffer_duration;
|
||||
log::debug!("sleep {:?}", sleep);
|
||||
thread::sleep(sleep);
|
||||
}
|
||||
|
||||
while let Ok(message) = player_rx.try_recv() {
|
||||
match message {
|
||||
PlayerMessage::SeekRelative(seek_seconds) => {
|
||||
if let Some(seconds) = seconds_opt {
|
||||
//TODO: use time base instead of hardcoded values
|
||||
let timestamp = ((seconds + seek_seconds) * 1000000.0) as i64;
|
||||
if seek_seconds.is_sign_negative() {
|
||||
println!(
|
||||
"backwards {} from {} = {}",
|
||||
seek_seconds, seconds, timestamp
|
||||
);
|
||||
ictx.seek(timestamp, ..timestamp)?;
|
||||
} else {
|
||||
println!("forwards {} from {} = {}", seek_seconds, seconds, timestamp);
|
||||
ictx.seek(timestamp, timestamp..)?;
|
||||
}
|
||||
|
||||
// Clear audio sync time
|
||||
sync_time_opt = None;
|
||||
// Clear audio and video queues
|
||||
{
|
||||
let mut audio_queue = audio_queue_lock.lock().unwrap();
|
||||
audio_queue.data.clear();
|
||||
}
|
||||
{
|
||||
//TODO: clear pending data stuck in channels
|
||||
let mut video_queue = video_queue_lock.lock().unwrap();
|
||||
video_queue.data.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
audio_decoder.send_eof()?;
|
||||
receive_and_process_decoded_audio_frames(&mut audio_decoder, &mut sync_time_opt)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run(path: PathBuf, config: Config) -> (mpsc::Sender<PlayerMessage>, Arc<Mutex<VideoQueue>>) {
|
||||
ffmpeg::init().unwrap();
|
||||
|
||||
let (player_tx, player_rx) = mpsc::channel();
|
||||
let video_queue_lock = Arc::new(Mutex::new(VideoQueue::new()));
|
||||
{
|
||||
let video_queue_lock = video_queue_lock.clone();
|
||||
thread::Builder::new()
|
||||
.name("ffmpeg".to_string())
|
||||
.spawn(move || {
|
||||
ffmpeg_thread(path, player_rx, video_queue_lock, config).unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
(player_tx, video_queue_lock)
|
||||
}
|
||||
|
|
@ -1,585 +0,0 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::{
|
||||
app::{message, Command, Core, Settings},
|
||||
cosmic_config::{self, CosmicConfigEntry},
|
||||
cosmic_theme, executor, font,
|
||||
iced::{
|
||||
event::{self, Event},
|
||||
keyboard::{Event as KeyEvent, Key, Modifiers},
|
||||
mouse::Event as MouseEvent,
|
||||
subscription::Subscription,
|
||||
window, Alignment, Color, Length, Limits,
|
||||
},
|
||||
theme,
|
||||
widget::{self, Slider},
|
||||
Application, ApplicationExt, Element,
|
||||
};
|
||||
use iced_video_player::{
|
||||
gst::{self, prelude::*},
|
||||
gst_pbutils, Video, VideoPlayer,
|
||||
};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
collections::HashMap,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::{Config, CONFIG_VERSION},
|
||||
key_bind::{key_binds, KeyBind},
|
||||
localize,
|
||||
};
|
||||
|
||||
static CONTROLS_TIMEOUT: Duration = Duration::new(2, 0);
|
||||
|
||||
const GST_PLAY_FLAG_VIDEO: i32 = 1 << 0;
|
||||
const GST_PLAY_FLAG_AUDIO: i32 = 1 << 1;
|
||||
const GST_PLAY_FLAG_TEXT: i32 = 1 << 2;
|
||||
|
||||
/// Runs application with these settings
|
||||
#[rustfmt::skip]
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
|
||||
|
||||
localize::localize();
|
||||
|
||||
let (config_handler, config) = match cosmic_config::Config::new(App::APP_ID, CONFIG_VERSION) {
|
||||
Ok(config_handler) => {
|
||||
let config = match Config::get_entry(&config_handler) {
|
||||
Ok(ok) => ok,
|
||||
Err((errs, config)) => {
|
||||
log::info!("errors loading config: {:?}", errs);
|
||||
config
|
||||
}
|
||||
};
|
||||
(Some(config_handler), config)
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("failed to create config handler: {}", err);
|
||||
(None, Config::default())
|
||||
}
|
||||
};
|
||||
|
||||
let mut settings = Settings::default();
|
||||
settings = settings.theme(config.app_theme.theme());
|
||||
settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0));
|
||||
|
||||
let url = url::Url::from_file_path(
|
||||
std::env::args().nth(1).unwrap()
|
||||
)
|
||||
.unwrap();
|
||||
let flags = Flags {
|
||||
config_handler,
|
||||
config,
|
||||
url,
|
||||
};
|
||||
cosmic::app::run::<App>(settings, flags)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Action {
|
||||
Fullscreen,
|
||||
PlayPause,
|
||||
SeekBackward,
|
||||
SeekForward,
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub fn message(&self) -> Message {
|
||||
match self {
|
||||
Self::Fullscreen => Message::Fullscreen,
|
||||
Self::PlayPause => Message::PlayPause,
|
||||
Self::SeekBackward => Message::SeekRelative(-10.0),
|
||||
Self::SeekForward => Message::SeekRelative(10.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Flags {
|
||||
config_handler: Option<cosmic_config::Config>,
|
||||
config: Config,
|
||||
url: url::Url,
|
||||
}
|
||||
|
||||
/// Messages that are used specifically by our [`App`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
Config(Config),
|
||||
Fullscreen,
|
||||
Key(Modifiers, Key),
|
||||
AudioCode(usize),
|
||||
TextCode(usize),
|
||||
PlayPause,
|
||||
Seek(f64),
|
||||
SeekRelative(f64),
|
||||
SeekRelease,
|
||||
EndOfStream,
|
||||
MissingPlugin(gst::Message),
|
||||
NewFrame,
|
||||
Reload,
|
||||
ShowControls,
|
||||
SystemThemeModeChange(cosmic_theme::ThemeMode),
|
||||
}
|
||||
|
||||
/// The [`App`] stores application-specific state.
|
||||
pub struct App {
|
||||
core: Core,
|
||||
flags: Flags,
|
||||
controls: bool,
|
||||
controls_time: Instant,
|
||||
fullscreen: bool,
|
||||
key_binds: HashMap<KeyBind, Action>,
|
||||
video_opt: Option<Video>,
|
||||
position: f64,
|
||||
duration: f64,
|
||||
dragging: bool,
|
||||
audio_codes: Vec<String>,
|
||||
current_audio: i32,
|
||||
text_codes: Vec<String>,
|
||||
current_text: i32,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn close(&mut self) {
|
||||
self.video_opt = None;
|
||||
self.position = 0.0;
|
||||
self.duration = 0.0;
|
||||
self.dragging = false;
|
||||
self.audio_codes = Vec::new();
|
||||
self.current_audio = -1;
|
||||
self.text_codes = Vec::new();
|
||||
self.current_text = -1;
|
||||
}
|
||||
|
||||
fn load(&mut self) -> Command<Message> {
|
||||
self.close();
|
||||
|
||||
let video = match Video::new(&self.flags.url) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
log::warn!("failed to open {:?}: {err}", self.flags.url);
|
||||
return Command::none();
|
||||
}
|
||||
};
|
||||
self.duration = video.duration().as_secs_f64();
|
||||
let pipeline = video.pipeline();
|
||||
self.video_opt = Some(video);
|
||||
|
||||
let n_audio = pipeline.property::<i32>("n-audio");
|
||||
self.audio_codes = Vec::with_capacity(n_audio as usize);
|
||||
for i in 0..n_audio {
|
||||
let tags: gst::TagList = pipeline.emit_by_name("get-audio-tags", &[&i]);
|
||||
log::info!("audio stream {i}: {tags:?}");
|
||||
self.audio_codes.push(
|
||||
if let Some(language_code) = tags.get::<gst::tags::LanguageCode>() {
|
||||
language_code.get().to_string()
|
||||
} else {
|
||||
format!("Audio #{i}")
|
||||
},
|
||||
);
|
||||
}
|
||||
self.current_audio = pipeline.property::<i32>("current-audio");
|
||||
|
||||
let n_text = pipeline.property::<i32>("n-text");
|
||||
self.text_codes = Vec::with_capacity(n_text as usize);
|
||||
for i in 0..n_text {
|
||||
let tags: gst::TagList = pipeline.emit_by_name("get-text-tags", &[&i]);
|
||||
log::info!("text stream {i}: {tags:?}");
|
||||
self.text_codes.push(
|
||||
if let Some(language_code) = tags.get::<gst::tags::LanguageCode>() {
|
||||
language_code.get().to_string()
|
||||
} else {
|
||||
format!("Subtitle #{i}")
|
||||
},
|
||||
);
|
||||
}
|
||||
self.current_text = pipeline.property::<i32>("current-text");
|
||||
|
||||
//TODO: Flags can be used to enable/disable subtitles
|
||||
let flags_value = pipeline.property_value("flags");
|
||||
println!("original flags {:?}", flags_value);
|
||||
match flags_value.transform::<i32>() {
|
||||
Ok(flags_transform) => match flags_transform.get::<i32>() {
|
||||
Ok(mut flags) => {
|
||||
flags |= GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_AUDIO | GST_PLAY_FLAG_TEXT;
|
||||
match gst::glib::Value::from(flags).transform_with_type(flags_value.type_()) {
|
||||
Ok(value) => pipeline.set_property("flags", value),
|
||||
Err(err) => {
|
||||
log::warn!("failed to transform int to flags: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("failed to get flags as int: {err}");
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
log::warn!("failed to transform flags to int: {err}");
|
||||
}
|
||||
}
|
||||
println!("updated flags {:?}", pipeline.property_value("flags"));
|
||||
|
||||
self.update_title()
|
||||
}
|
||||
|
||||
fn update_controls(&mut self, in_use: bool) {
|
||||
if in_use {
|
||||
self.controls = true;
|
||||
self.controls_time = Instant::now();
|
||||
} else if self.controls && self.controls_time.elapsed() > CONTROLS_TIMEOUT {
|
||||
self.controls = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_config(&mut self) -> Command<Message> {
|
||||
cosmic::app::command::set_theme(self.flags.config.app_theme.theme())
|
||||
}
|
||||
|
||||
fn update_title(&mut self) -> Command<Message> {
|
||||
//TODO: filename?
|
||||
let title = "COSMIC Media Player";
|
||||
self.set_window_title(title.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement [`cosmic::Application`] to integrate with COSMIC.
|
||||
impl Application for App {
|
||||
/// Default async executor to use with the app.
|
||||
type Executor = executor::Default;
|
||||
|
||||
/// Argument received [`cosmic::Application::new`].
|
||||
type Flags = Flags;
|
||||
|
||||
/// Message type specific to our [`App`].
|
||||
type Message = Message;
|
||||
|
||||
/// The unique application ID to supply to the window manager.
|
||||
const APP_ID: &'static str = "com.system76.CosmicPlayer";
|
||||
|
||||
fn core(&self) -> &Core {
|
||||
&self.core
|
||||
}
|
||||
|
||||
fn core_mut(&mut self) -> &mut Core {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
/// Creates the application, and optionally emits command on initialize.
|
||||
fn init(mut core: Core, flags: Self::Flags) -> (Self, Command<Self::Message>) {
|
||||
core.window.content_container = false;
|
||||
|
||||
let mut app = App {
|
||||
core,
|
||||
flags,
|
||||
controls: true,
|
||||
controls_time: Instant::now(),
|
||||
fullscreen: false,
|
||||
key_binds: key_binds(),
|
||||
video_opt: None,
|
||||
position: 0.0,
|
||||
duration: 0.0,
|
||||
dragging: false,
|
||||
audio_codes: Vec::new(),
|
||||
current_audio: -1,
|
||||
text_codes: Vec::new(),
|
||||
current_text: -1,
|
||||
};
|
||||
|
||||
let command = app.load();
|
||||
(app, command)
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> Command<Self::Message> {
|
||||
if self.fullscreen {
|
||||
return self.update(Message::Fullscreen);
|
||||
} else {
|
||||
Command::none()
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle application events here.
|
||||
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
|
||||
match message {
|
||||
Message::Config(config) => {
|
||||
if config != self.flags.config {
|
||||
log::info!("update config");
|
||||
self.flags.config = config;
|
||||
return self.update_config();
|
||||
}
|
||||
}
|
||||
Message::Fullscreen => {
|
||||
self.fullscreen = !self.fullscreen;
|
||||
self.core.window.show_headerbar = !self.fullscreen;
|
||||
return window::change_mode(
|
||||
window::Id::MAIN,
|
||||
if self.fullscreen {
|
||||
window::Mode::Fullscreen
|
||||
} else {
|
||||
window::Mode::Windowed
|
||||
},
|
||||
);
|
||||
}
|
||||
Message::Key(modifiers, key) => {
|
||||
for (key_bind, action) in self.key_binds.iter() {
|
||||
if key_bind.matches(modifiers, &key) {
|
||||
return self.update(action.message());
|
||||
}
|
||||
}
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::TextCode(code) => {
|
||||
if let Ok(code) = i32::try_from(code) {
|
||||
if let Some(video) = &self.video_opt {
|
||||
let pipeline = video.pipeline();
|
||||
pipeline.set_property("current-text", code);
|
||||
self.current_text = pipeline.property("current-text");
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::PlayPause => {
|
||||
if let Some(video) = &mut self.video_opt {
|
||||
video.set_paused(!video.paused());
|
||||
self.update_controls(true);
|
||||
}
|
||||
}
|
||||
Message::Seek(secs) => {
|
||||
if let Some(video) = &mut self.video_opt {
|
||||
self.dragging = true;
|
||||
self.position = secs;
|
||||
video.set_paused(true);
|
||||
let duration = Duration::try_from_secs_f64(self.position).unwrap_or_default();
|
||||
video.seek(duration, true).expect("seek");
|
||||
self.update_controls(true);
|
||||
}
|
||||
}
|
||||
Message::SeekRelative(secs) => {
|
||||
if let Some(video) = &mut self.video_opt {
|
||||
self.position = video.position().as_secs_f64();
|
||||
let duration =
|
||||
Duration::try_from_secs_f64(self.position + secs).unwrap_or_default();
|
||||
video.seek(duration, true).expect("seek");
|
||||
}
|
||||
}
|
||||
Message::SeekRelease => {
|
||||
if let Some(video) = &mut self.video_opt {
|
||||
self.dragging = false;
|
||||
let duration = Duration::try_from_secs_f64(self.position).unwrap_or_default();
|
||||
video.seek(duration, true).expect("seek");
|
||||
video.set_paused(false);
|
||||
self.update_controls(true);
|
||||
}
|
||||
}
|
||||
Message::EndOfStream => {
|
||||
println!("end of stream");
|
||||
}
|
||||
Message::MissingPlugin(element) => {
|
||||
if let Some(video) = &mut self.video_opt {
|
||||
video.set_paused(true);
|
||||
}
|
||||
return Command::perform(
|
||||
async move {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
match gst_pbutils::MissingPluginMessage::parse(&element) {
|
||||
Ok(missing_plugin) => {
|
||||
let mut install_ctx = gst_pbutils::InstallPluginsContext::new();
|
||||
install_ctx
|
||||
.set_desktop_id(&format!("{}.desktop", Self::APP_ID));
|
||||
let install_detail = missing_plugin.installer_detail();
|
||||
println!("installing plugins: {}", install_detail);
|
||||
let status = gst_pbutils::missing_plugins::install_plugins_sync(
|
||||
&[&install_detail],
|
||||
Some(&install_ctx),
|
||||
);
|
||||
log::info!("plugin install status: {}", status);
|
||||
log::info!(
|
||||
"gstreamer registry update: {:?}",
|
||||
gst::Registry::update()
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("failed to parse missing plugin message: {err}");
|
||||
}
|
||||
}
|
||||
message::app(Message::Reload)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
},
|
||||
|x| x,
|
||||
);
|
||||
}
|
||||
Message::NewFrame => {
|
||||
if let Some(video) = &self.video_opt {
|
||||
if !self.dragging {
|
||||
self.position = video.position().as_secs_f64();
|
||||
self.update_controls(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Reload => {
|
||||
return self.load();
|
||||
}
|
||||
Message::ShowControls => {
|
||||
self.update_controls(true);
|
||||
}
|
||||
Message::SystemThemeModeChange(_theme_mode) => {
|
||||
return self.update_config();
|
||||
}
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn header_start(&self) -> Vec<Element<Self::Message>> {
|
||||
let mut row = widget::row::with_capacity(4)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(8);
|
||||
if !self.audio_codes.is_empty() {
|
||||
//TODO: allow mute/unmute/change volume
|
||||
row = row.push(widget::icon::from_name("audio-volume-high-symbolic").size(16));
|
||||
row = row.push(widget::dropdown(
|
||||
&self.audio_codes,
|
||||
usize::try_from(self.current_audio).ok(),
|
||||
Message::AudioCode,
|
||||
));
|
||||
}
|
||||
if !self.text_codes.is_empty() {
|
||||
//TODO: allow toggling subtitles
|
||||
row = row.push(widget::icon::from_name("media-view-subtitles-symbolic").size(16));
|
||||
row = row.push(widget::dropdown(
|
||||
&self.text_codes,
|
||||
usize::try_from(self.current_text).ok(),
|
||||
Message::TextCode,
|
||||
));
|
||||
}
|
||||
vec![row.into()]
|
||||
}
|
||||
|
||||
/// Creates a view after each update.
|
||||
fn view(&self) -> Element<Self::Message> {
|
||||
let format_time = |time_float: f64| -> String {
|
||||
let time = time_float.floor() as i64;
|
||||
let seconds = time % 60;
|
||||
let minutes = (time / 60) % 60;
|
||||
let hours = (time / 60) / 60;
|
||||
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
|
||||
};
|
||||
|
||||
let Some(video) = &self.video_opt else {
|
||||
//TODO: open button if no video?
|
||||
return widget::container(widget::text("No video open"))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.style(theme::Container::WindowBackground)
|
||||
.into();
|
||||
};
|
||||
|
||||
let video_player = VideoPlayer::new(video)
|
||||
.mouse_hidden(!self.controls)
|
||||
.on_end_of_stream(Message::EndOfStream)
|
||||
.on_missing_plugin(Message::MissingPlugin)
|
||||
.on_new_frame(Message::NewFrame)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill);
|
||||
|
||||
let mouse_area = widget::mouse_area(video_player).on_double_press(Message::Fullscreen);
|
||||
|
||||
let mut popover = widget::popover(mouse_area).position(widget::popover::Position::Bottom);
|
||||
if self.controls {
|
||||
popover = popover.popup(
|
||||
widget::container(
|
||||
widget::row::with_capacity(5)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(8)
|
||||
.padding([0, 8])
|
||||
.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),
|
||||
)
|
||||
.push(widget::text(format_time(self.position)).font(font::mono()))
|
||||
.push(
|
||||
Slider::new(0.0..=self.duration, self.position, Message::Seek)
|
||||
.step(0.1)
|
||||
.on_release(Message::SeekRelease),
|
||||
)
|
||||
.push(
|
||||
widget::text(format_time(self.duration - self.position))
|
||||
.font(font::mono()),
|
||||
)
|
||||
.push(
|
||||
widget::button::icon(
|
||||
widget::icon::from_name("view-fullscreen-symbolic").size(16),
|
||||
)
|
||||
.on_press(Message::Fullscreen),
|
||||
),
|
||||
)
|
||||
.style(theme::Container::WindowBackground),
|
||||
);
|
||||
}
|
||||
|
||||
widget::container(popover)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.style(theme::Container::Custom(Box::new(|_theme| {
|
||||
widget::container::Appearance::default().with_background(Color::BLACK)
|
||||
})))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Self::Message> {
|
||||
struct ConfigSubscription;
|
||||
struct ThemeSubscription;
|
||||
|
||||
Subscription::batch([
|
||||
event::listen_with(|event, _status| match event {
|
||||
Event::Keyboard(KeyEvent::KeyPressed { key, modifiers, .. }) => {
|
||||
Some(Message::Key(modifiers, key))
|
||||
}
|
||||
Event::Mouse(MouseEvent::CursorMoved { .. }) => Some(Message::ShowControls),
|
||||
_ => None,
|
||||
}),
|
||||
cosmic_config::config_subscription(
|
||||
TypeId::of::<ConfigSubscription>(),
|
||||
Self::APP_ID.into(),
|
||||
CONFIG_VERSION,
|
||||
)
|
||||
.map(|update| {
|
||||
if !update.errors.is_empty() {
|
||||
log::debug!("errors loading config: {:?}", update.errors);
|
||||
}
|
||||
Message::SystemThemeModeChange(update.config)
|
||||
}),
|
||||
cosmic_config::config_subscription::<_, cosmic_theme::ThemeMode>(
|
||||
TypeId::of::<ThemeSubscription>(),
|
||||
cosmic_theme::THEME_MODE_ID.into(),
|
||||
cosmic_theme::ThemeMode::version(),
|
||||
)
|
||||
.map(|update| {
|
||||
if !update.errors.is_empty() {
|
||||
log::debug!("errors loading theme mode: {:?}", update.errors);
|
||||
}
|
||||
Message::SystemThemeModeChange(update.config)
|
||||
}),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ use cosmic::{
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, fmt};
|
||||
|
||||
use crate::app::Action;
|
||||
use crate::Action;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
||||
pub enum Modifier {
|
||||
|
|
|
|||
586
src/main.rs
586
src/main.rs
|
|
@ -1,18 +1,588 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::{
|
||||
app::{message, Command, Core, Settings},
|
||||
cosmic_config::{self, CosmicConfigEntry},
|
||||
cosmic_theme, executor, font,
|
||||
iced::{
|
||||
event::{self, Event},
|
||||
keyboard::{Event as KeyEvent, Key, Modifiers},
|
||||
mouse::Event as MouseEvent,
|
||||
subscription::Subscription,
|
||||
window, Alignment, Color, Length, Limits,
|
||||
},
|
||||
theme,
|
||||
widget::{self, Slider},
|
||||
Application, ApplicationExt, Element,
|
||||
};
|
||||
use iced_video_player::{
|
||||
gst::{self, prelude::*},
|
||||
gst_pbutils, Video, VideoPlayer,
|
||||
};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
collections::HashMap,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::{Config, CONFIG_VERSION},
|
||||
key_bind::{key_binds, KeyBind},
|
||||
};
|
||||
|
||||
mod config;
|
||||
mod key_bind;
|
||||
mod localize;
|
||||
|
||||
#[cfg(feature = "ffmpeg")]
|
||||
#[path = "ffmpeg/mod.rs"]
|
||||
mod app;
|
||||
static CONTROLS_TIMEOUT: Duration = Duration::new(2, 0);
|
||||
|
||||
#[cfg(feature = "gstreamer")]
|
||||
#[path = "gstreamer/mod.rs"]
|
||||
mod app;
|
||||
const GST_PLAY_FLAG_VIDEO: i32 = 1 << 0;
|
||||
const GST_PLAY_FLAG_AUDIO: i32 = 1 << 1;
|
||||
const GST_PLAY_FLAG_TEXT: i32 = 1 << 2;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
app::main()
|
||||
/// Runs application with these settings
|
||||
#[rustfmt::skip]
|
||||
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init();
|
||||
|
||||
localize::localize();
|
||||
|
||||
let (config_handler, config) = match cosmic_config::Config::new(App::APP_ID, CONFIG_VERSION) {
|
||||
Ok(config_handler) => {
|
||||
let config = match Config::get_entry(&config_handler) {
|
||||
Ok(ok) => ok,
|
||||
Err((errs, config)) => {
|
||||
log::info!("errors loading config: {:?}", errs);
|
||||
config
|
||||
}
|
||||
};
|
||||
(Some(config_handler), config)
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("failed to create config handler: {}", err);
|
||||
(None, Config::default())
|
||||
}
|
||||
};
|
||||
|
||||
let mut settings = Settings::default();
|
||||
settings = settings.theme(config.app_theme.theme());
|
||||
settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0));
|
||||
|
||||
let url = url::Url::from_file_path(
|
||||
std::env::args().nth(1).unwrap()
|
||||
)
|
||||
.unwrap();
|
||||
let flags = Flags {
|
||||
config_handler,
|
||||
config,
|
||||
url,
|
||||
};
|
||||
cosmic::app::run::<App>(settings, flags)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Action {
|
||||
Fullscreen,
|
||||
PlayPause,
|
||||
SeekBackward,
|
||||
SeekForward,
|
||||
}
|
||||
|
||||
impl Action {
|
||||
pub fn message(&self) -> Message {
|
||||
match self {
|
||||
Self::Fullscreen => Message::Fullscreen,
|
||||
Self::PlayPause => Message::PlayPause,
|
||||
Self::SeekBackward => Message::SeekRelative(-10.0),
|
||||
Self::SeekForward => Message::SeekRelative(10.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Flags {
|
||||
config_handler: Option<cosmic_config::Config>,
|
||||
config: Config,
|
||||
url: url::Url,
|
||||
}
|
||||
|
||||
/// Messages that are used specifically by our [`App`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Message {
|
||||
Config(Config),
|
||||
Fullscreen,
|
||||
Key(Modifiers, Key),
|
||||
AudioCode(usize),
|
||||
TextCode(usize),
|
||||
PlayPause,
|
||||
Seek(f64),
|
||||
SeekRelative(f64),
|
||||
SeekRelease,
|
||||
EndOfStream,
|
||||
MissingPlugin(gst::Message),
|
||||
NewFrame,
|
||||
Reload,
|
||||
ShowControls,
|
||||
SystemThemeModeChange(cosmic_theme::ThemeMode),
|
||||
}
|
||||
|
||||
/// The [`App`] stores application-specific state.
|
||||
pub struct App {
|
||||
core: Core,
|
||||
flags: Flags,
|
||||
controls: bool,
|
||||
controls_time: Instant,
|
||||
fullscreen: bool,
|
||||
key_binds: HashMap<KeyBind, Action>,
|
||||
video_opt: Option<Video>,
|
||||
position: f64,
|
||||
duration: f64,
|
||||
dragging: bool,
|
||||
audio_codes: Vec<String>,
|
||||
current_audio: i32,
|
||||
text_codes: Vec<String>,
|
||||
current_text: i32,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn close(&mut self) {
|
||||
self.video_opt = None;
|
||||
self.position = 0.0;
|
||||
self.duration = 0.0;
|
||||
self.dragging = false;
|
||||
self.audio_codes = Vec::new();
|
||||
self.current_audio = -1;
|
||||
self.text_codes = Vec::new();
|
||||
self.current_text = -1;
|
||||
}
|
||||
|
||||
fn load(&mut self) -> Command<Message> {
|
||||
self.close();
|
||||
|
||||
let video = match Video::new(&self.flags.url) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
log::warn!("failed to open {:?}: {err}", self.flags.url);
|
||||
return Command::none();
|
||||
}
|
||||
};
|
||||
self.duration = video.duration().as_secs_f64();
|
||||
let pipeline = video.pipeline();
|
||||
self.video_opt = Some(video);
|
||||
|
||||
let n_audio = pipeline.property::<i32>("n-audio");
|
||||
self.audio_codes = Vec::with_capacity(n_audio as usize);
|
||||
for i in 0..n_audio {
|
||||
let tags: gst::TagList = pipeline.emit_by_name("get-audio-tags", &[&i]);
|
||||
log::info!("audio stream {i}: {tags:?}");
|
||||
self.audio_codes.push(
|
||||
if let Some(language_code) = tags.get::<gst::tags::LanguageCode>() {
|
||||
language_code.get().to_string()
|
||||
} else {
|
||||
format!("Audio #{i}")
|
||||
},
|
||||
);
|
||||
}
|
||||
self.current_audio = pipeline.property::<i32>("current-audio");
|
||||
|
||||
let n_text = pipeline.property::<i32>("n-text");
|
||||
self.text_codes = Vec::with_capacity(n_text as usize);
|
||||
for i in 0..n_text {
|
||||
let tags: gst::TagList = pipeline.emit_by_name("get-text-tags", &[&i]);
|
||||
log::info!("text stream {i}: {tags:?}");
|
||||
self.text_codes.push(
|
||||
if let Some(language_code) = tags.get::<gst::tags::LanguageCode>() {
|
||||
language_code.get().to_string()
|
||||
} else {
|
||||
format!("Subtitle #{i}")
|
||||
},
|
||||
);
|
||||
}
|
||||
self.current_text = pipeline.property::<i32>("current-text");
|
||||
|
||||
//TODO: Flags can be used to enable/disable subtitles
|
||||
let flags_value = pipeline.property_value("flags");
|
||||
println!("original flags {:?}", flags_value);
|
||||
match flags_value.transform::<i32>() {
|
||||
Ok(flags_transform) => match flags_transform.get::<i32>() {
|
||||
Ok(mut flags) => {
|
||||
flags |= GST_PLAY_FLAG_VIDEO | GST_PLAY_FLAG_AUDIO | GST_PLAY_FLAG_TEXT;
|
||||
match gst::glib::Value::from(flags).transform_with_type(flags_value.type_()) {
|
||||
Ok(value) => pipeline.set_property("flags", value),
|
||||
Err(err) => {
|
||||
log::warn!("failed to transform int to flags: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("failed to get flags as int: {err}");
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
log::warn!("failed to transform flags to int: {err}");
|
||||
}
|
||||
}
|
||||
println!("updated flags {:?}", pipeline.property_value("flags"));
|
||||
|
||||
self.update_title()
|
||||
}
|
||||
|
||||
fn update_controls(&mut self, in_use: bool) {
|
||||
if in_use {
|
||||
self.controls = true;
|
||||
self.controls_time = Instant::now();
|
||||
} else if self.controls && self.controls_time.elapsed() > CONTROLS_TIMEOUT {
|
||||
self.controls = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_config(&mut self) -> Command<Message> {
|
||||
cosmic::app::command::set_theme(self.flags.config.app_theme.theme())
|
||||
}
|
||||
|
||||
fn update_title(&mut self) -> Command<Message> {
|
||||
//TODO: filename?
|
||||
let title = "COSMIC Media Player";
|
||||
self.set_window_title(title.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement [`cosmic::Application`] to integrate with COSMIC.
|
||||
impl Application for App {
|
||||
/// Default async executor to use with the app.
|
||||
type Executor = executor::Default;
|
||||
|
||||
/// Argument received [`cosmic::Application::new`].
|
||||
type Flags = Flags;
|
||||
|
||||
/// Message type specific to our [`App`].
|
||||
type Message = Message;
|
||||
|
||||
/// The unique application ID to supply to the window manager.
|
||||
const APP_ID: &'static str = "com.system76.CosmicPlayer";
|
||||
|
||||
fn core(&self) -> &Core {
|
||||
&self.core
|
||||
}
|
||||
|
||||
fn core_mut(&mut self) -> &mut Core {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
/// Creates the application, and optionally emits command on initialize.
|
||||
fn init(mut core: Core, flags: Self::Flags) -> (Self, Command<Self::Message>) {
|
||||
core.window.content_container = false;
|
||||
|
||||
let mut app = App {
|
||||
core,
|
||||
flags,
|
||||
controls: true,
|
||||
controls_time: Instant::now(),
|
||||
fullscreen: false,
|
||||
key_binds: key_binds(),
|
||||
video_opt: None,
|
||||
position: 0.0,
|
||||
duration: 0.0,
|
||||
dragging: false,
|
||||
audio_codes: Vec::new(),
|
||||
current_audio: -1,
|
||||
text_codes: Vec::new(),
|
||||
current_text: -1,
|
||||
};
|
||||
|
||||
let command = app.load();
|
||||
(app, command)
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> Command<Self::Message> {
|
||||
if self.fullscreen {
|
||||
return self.update(Message::Fullscreen);
|
||||
} else {
|
||||
Command::none()
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle application events here.
|
||||
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
|
||||
match message {
|
||||
Message::Config(config) => {
|
||||
if config != self.flags.config {
|
||||
log::info!("update config");
|
||||
self.flags.config = config;
|
||||
return self.update_config();
|
||||
}
|
||||
}
|
||||
Message::Fullscreen => {
|
||||
self.fullscreen = !self.fullscreen;
|
||||
self.core.window.show_headerbar = !self.fullscreen;
|
||||
return window::change_mode(
|
||||
window::Id::MAIN,
|
||||
if self.fullscreen {
|
||||
window::Mode::Fullscreen
|
||||
} else {
|
||||
window::Mode::Windowed
|
||||
},
|
||||
);
|
||||
}
|
||||
Message::Key(modifiers, key) => {
|
||||
for (key_bind, action) in self.key_binds.iter() {
|
||||
if key_bind.matches(modifiers, &key) {
|
||||
return self.update(action.message());
|
||||
}
|
||||
}
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::TextCode(code) => {
|
||||
if let Ok(code) = i32::try_from(code) {
|
||||
if let Some(video) = &self.video_opt {
|
||||
let pipeline = video.pipeline();
|
||||
pipeline.set_property("current-text", code);
|
||||
self.current_text = pipeline.property("current-text");
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::PlayPause => {
|
||||
if let Some(video) = &mut self.video_opt {
|
||||
video.set_paused(!video.paused());
|
||||
self.update_controls(true);
|
||||
}
|
||||
}
|
||||
Message::Seek(secs) => {
|
||||
if let Some(video) = &mut self.video_opt {
|
||||
self.dragging = true;
|
||||
self.position = secs;
|
||||
video.set_paused(true);
|
||||
let duration = Duration::try_from_secs_f64(self.position).unwrap_or_default();
|
||||
video.seek(duration, true).expect("seek");
|
||||
self.update_controls(true);
|
||||
}
|
||||
}
|
||||
Message::SeekRelative(secs) => {
|
||||
if let Some(video) = &mut self.video_opt {
|
||||
self.position = video.position().as_secs_f64();
|
||||
let duration =
|
||||
Duration::try_from_secs_f64(self.position + secs).unwrap_or_default();
|
||||
video.seek(duration, true).expect("seek");
|
||||
}
|
||||
}
|
||||
Message::SeekRelease => {
|
||||
if let Some(video) = &mut self.video_opt {
|
||||
self.dragging = false;
|
||||
let duration = Duration::try_from_secs_f64(self.position).unwrap_or_default();
|
||||
video.seek(duration, true).expect("seek");
|
||||
video.set_paused(false);
|
||||
self.update_controls(true);
|
||||
}
|
||||
}
|
||||
Message::EndOfStream => {
|
||||
println!("end of stream");
|
||||
}
|
||||
Message::MissingPlugin(element) => {
|
||||
if let Some(video) = &mut self.video_opt {
|
||||
video.set_paused(true);
|
||||
}
|
||||
return Command::perform(
|
||||
async move {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
match gst_pbutils::MissingPluginMessage::parse(&element) {
|
||||
Ok(missing_plugin) => {
|
||||
let mut install_ctx = gst_pbutils::InstallPluginsContext::new();
|
||||
install_ctx
|
||||
.set_desktop_id(&format!("{}.desktop", Self::APP_ID));
|
||||
let install_detail = missing_plugin.installer_detail();
|
||||
println!("installing plugins: {}", install_detail);
|
||||
let status = gst_pbutils::missing_plugins::install_plugins_sync(
|
||||
&[&install_detail],
|
||||
Some(&install_ctx),
|
||||
);
|
||||
log::info!("plugin install status: {}", status);
|
||||
log::info!(
|
||||
"gstreamer registry update: {:?}",
|
||||
gst::Registry::update()
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("failed to parse missing plugin message: {err}");
|
||||
}
|
||||
}
|
||||
message::app(Message::Reload)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
},
|
||||
|x| x,
|
||||
);
|
||||
}
|
||||
Message::NewFrame => {
|
||||
if let Some(video) = &self.video_opt {
|
||||
if !self.dragging {
|
||||
self.position = video.position().as_secs_f64();
|
||||
self.update_controls(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Reload => {
|
||||
return self.load();
|
||||
}
|
||||
Message::ShowControls => {
|
||||
self.update_controls(true);
|
||||
}
|
||||
Message::SystemThemeModeChange(_theme_mode) => {
|
||||
return self.update_config();
|
||||
}
|
||||
}
|
||||
Command::none()
|
||||
}
|
||||
|
||||
fn header_start(&self) -> Vec<Element<Self::Message>> {
|
||||
let mut row = widget::row::with_capacity(4)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(8);
|
||||
if !self.audio_codes.is_empty() {
|
||||
//TODO: allow mute/unmute/change volume
|
||||
row = row.push(widget::icon::from_name("audio-volume-high-symbolic").size(16));
|
||||
row = row.push(widget::dropdown(
|
||||
&self.audio_codes,
|
||||
usize::try_from(self.current_audio).ok(),
|
||||
Message::AudioCode,
|
||||
));
|
||||
}
|
||||
if !self.text_codes.is_empty() {
|
||||
//TODO: allow toggling subtitles
|
||||
row = row.push(widget::icon::from_name("media-view-subtitles-symbolic").size(16));
|
||||
row = row.push(widget::dropdown(
|
||||
&self.text_codes,
|
||||
usize::try_from(self.current_text).ok(),
|
||||
Message::TextCode,
|
||||
));
|
||||
}
|
||||
vec![row.into()]
|
||||
}
|
||||
|
||||
/// Creates a view after each update.
|
||||
fn view(&self) -> Element<Self::Message> {
|
||||
let format_time = |time_float: f64| -> String {
|
||||
let time = time_float.floor() as i64;
|
||||
let seconds = time % 60;
|
||||
let minutes = (time / 60) % 60;
|
||||
let hours = (time / 60) / 60;
|
||||
format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
|
||||
};
|
||||
|
||||
let Some(video) = &self.video_opt else {
|
||||
//TODO: open button if no video?
|
||||
return widget::container(widget::text("No video open"))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.style(theme::Container::WindowBackground)
|
||||
.into();
|
||||
};
|
||||
|
||||
let video_player = VideoPlayer::new(video)
|
||||
.mouse_hidden(!self.controls)
|
||||
.on_end_of_stream(Message::EndOfStream)
|
||||
.on_missing_plugin(Message::MissingPlugin)
|
||||
.on_new_frame(Message::NewFrame)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill);
|
||||
|
||||
let mouse_area = widget::mouse_area(video_player).on_double_press(Message::Fullscreen);
|
||||
|
||||
let mut popover = widget::popover(mouse_area).position(widget::popover::Position::Bottom);
|
||||
if self.controls {
|
||||
popover = popover.popup(
|
||||
widget::container(
|
||||
widget::row::with_capacity(5)
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(8)
|
||||
.padding([0, 8])
|
||||
.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),
|
||||
)
|
||||
.push(widget::text(format_time(self.position)).font(font::mono()))
|
||||
.push(
|
||||
Slider::new(0.0..=self.duration, self.position, Message::Seek)
|
||||
.step(0.1)
|
||||
.on_release(Message::SeekRelease),
|
||||
)
|
||||
.push(
|
||||
widget::text(format_time(self.duration - self.position))
|
||||
.font(font::mono()),
|
||||
)
|
||||
.push(
|
||||
widget::button::icon(
|
||||
widget::icon::from_name("view-fullscreen-symbolic").size(16),
|
||||
)
|
||||
.on_press(Message::Fullscreen),
|
||||
),
|
||||
)
|
||||
.style(theme::Container::WindowBackground),
|
||||
);
|
||||
}
|
||||
|
||||
widget::container(popover)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.style(theme::Container::Custom(Box::new(|_theme| {
|
||||
widget::container::Appearance::default().with_background(Color::BLACK)
|
||||
})))
|
||||
.into()
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Self::Message> {
|
||||
struct ConfigSubscription;
|
||||
struct ThemeSubscription;
|
||||
|
||||
Subscription::batch([
|
||||
event::listen_with(|event, _status| match event {
|
||||
Event::Keyboard(KeyEvent::KeyPressed { key, modifiers, .. }) => {
|
||||
Some(Message::Key(modifiers, key))
|
||||
}
|
||||
Event::Mouse(MouseEvent::CursorMoved { .. }) => Some(Message::ShowControls),
|
||||
_ => None,
|
||||
}),
|
||||
cosmic_config::config_subscription(
|
||||
TypeId::of::<ConfigSubscription>(),
|
||||
Self::APP_ID.into(),
|
||||
CONFIG_VERSION,
|
||||
)
|
||||
.map(|update| {
|
||||
if !update.errors.is_empty() {
|
||||
log::debug!("errors loading config: {:?}", update.errors);
|
||||
}
|
||||
Message::SystemThemeModeChange(update.config)
|
||||
}),
|
||||
cosmic_config::config_subscription::<_, cosmic_theme::ThemeMode>(
|
||||
TypeId::of::<ThemeSubscription>(),
|
||||
cosmic_theme::THEME_MODE_ID.into(),
|
||||
cosmic_theme::ThemeMode::version(),
|
||||
)
|
||||
.map(|update| {
|
||||
if !update.errors.is_empty() {
|
||||
log::debug!("errors loading theme mode: {:?}", update.errors);
|
||||
}
|
||||
Message::SystemThemeModeChange(update.config)
|
||||
}),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue