├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── data ├── com.igalia.GstWPEBroadcastDemo.appdata.xml ├── com.igalia.GstWPEBroadcastDemo.desktop ├── com.igalia.GstWPEBroadcastDemo.svg ├── gst-logo.svg ├── igalia-logo.png ├── index.html ├── screenshot.png └── style.css ├── meson.build ├── scripts ├── cargo.sh ├── grabber.sh ├── meson_post_install.py └── release.sh └── src ├── about_dialog.rs ├── app.rs ├── audio_vumeter.rs ├── header_bar.rs ├── macros.rs ├── main.rs ├── pipeline.rs ├── settings.rs └── utils.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "atk" 5 | version = "0.8.0" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | dependencies = [ 8 | "atk-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 9 | "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 10 | "glib 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", 11 | "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 12 | "gobject-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 13 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 14 | ] 15 | 16 | [[package]] 17 | name = "atk-sys" 18 | version = "0.9.1" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | dependencies = [ 21 | "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 22 | "gobject-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 23 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 24 | "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", 25 | ] 26 | 27 | [[package]] 28 | name = "autocfg" 29 | version = "1.0.0" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | 32 | [[package]] 33 | name = "backtrace" 34 | version = "0.3.46" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | dependencies = [ 37 | "backtrace-sys 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", 38 | "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", 39 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 40 | "rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", 41 | ] 42 | 43 | [[package]] 44 | name = "backtrace-sys" 45 | version = "0.1.35" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | dependencies = [ 48 | "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", 49 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 50 | ] 51 | 52 | [[package]] 53 | name = "base64" 54 | version = "0.9.3" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | dependencies = [ 57 | "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 58 | "safemem 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 59 | ] 60 | 61 | [[package]] 62 | name = "base64" 63 | version = "0.11.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | 66 | [[package]] 67 | name = "bitflags" 68 | version = "0.9.1" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | 71 | [[package]] 72 | name = "bitflags" 73 | version = "1.2.1" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | 76 | [[package]] 77 | name = "byteorder" 78 | version = "1.3.4" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | 81 | [[package]] 82 | name = "cairo-rs" 83 | version = "0.8.1" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | dependencies = [ 86 | "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 87 | "cairo-sys-rs 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", 88 | "glib 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", 89 | "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 90 | "gobject-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 91 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 92 | ] 93 | 94 | [[package]] 95 | name = "cairo-sys-rs" 96 | version = "0.9.2" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | dependencies = [ 99 | "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 100 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 101 | "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", 102 | ] 103 | 104 | [[package]] 105 | name = "cc" 106 | version = "1.0.50" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | 109 | [[package]] 110 | name = "cfg-if" 111 | version = "0.1.10" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | 114 | [[package]] 115 | name = "dtoa" 116 | version = "0.4.5" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | 119 | [[package]] 120 | name = "error-chain" 121 | version = "0.10.0" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | dependencies = [ 124 | "backtrace 0.3.46 (registry+https://github.com/rust-lang/crates.io-index)", 125 | ] 126 | 127 | [[package]] 128 | name = "failure" 129 | version = "0.1.7" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | dependencies = [ 132 | "backtrace 0.3.46 (registry+https://github.com/rust-lang/crates.io-index)", 133 | "failure_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 134 | ] 135 | 136 | [[package]] 137 | name = "failure_derive" 138 | version = "0.1.7" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | dependencies = [ 141 | "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", 142 | "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", 143 | "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", 144 | "synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)", 145 | ] 146 | 147 | [[package]] 148 | name = "futures-channel" 149 | version = "0.3.4" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | dependencies = [ 152 | "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 153 | ] 154 | 155 | [[package]] 156 | name = "futures-core" 157 | version = "0.3.4" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | 160 | [[package]] 161 | name = "futures-executor" 162 | version = "0.3.4" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | dependencies = [ 165 | "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 166 | "futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 167 | "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 168 | ] 169 | 170 | [[package]] 171 | name = "futures-io" 172 | version = "0.3.4" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | 175 | [[package]] 176 | name = "futures-macro" 177 | version = "0.3.4" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | dependencies = [ 180 | "proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)", 181 | "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", 182 | "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", 183 | "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", 184 | ] 185 | 186 | [[package]] 187 | name = "futures-task" 188 | version = "0.3.4" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | 191 | [[package]] 192 | name = "futures-util" 193 | version = "0.3.4" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | dependencies = [ 196 | "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 197 | "futures-macro 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 198 | "futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 199 | "pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)", 200 | "proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)", 201 | "proc-macro-nested 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 202 | "slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 203 | ] 204 | 205 | [[package]] 206 | name = "gdk" 207 | version = "0.12.1" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | dependencies = [ 210 | "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 211 | "cairo-rs 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", 212 | "cairo-sys-rs 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", 213 | "gdk-pixbuf 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", 214 | "gdk-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 215 | "gio 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", 216 | "gio-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 217 | "glib 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", 218 | "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 219 | "gobject-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 220 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 221 | "pango 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", 222 | ] 223 | 224 | [[package]] 225 | name = "gdk-pixbuf" 226 | version = "0.8.0" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | dependencies = [ 229 | "gdk-pixbuf-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 230 | "gio 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", 231 | "gio-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 232 | "glib 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", 233 | "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 234 | "gobject-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 235 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 236 | ] 237 | 238 | [[package]] 239 | name = "gdk-pixbuf-sys" 240 | version = "0.9.1" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | dependencies = [ 243 | "gio-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 244 | "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 245 | "gobject-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 246 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 247 | "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", 248 | ] 249 | 250 | [[package]] 251 | name = "gdk-sys" 252 | version = "0.9.1" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | dependencies = [ 255 | "cairo-sys-rs 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", 256 | "gdk-pixbuf-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 257 | "gio-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 258 | "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 259 | "gobject-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 260 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 261 | "pango-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 262 | "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", 263 | ] 264 | 265 | [[package]] 266 | name = "gio" 267 | version = "0.8.1" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | dependencies = [ 270 | "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 271 | "futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 272 | "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 273 | "futures-io 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 274 | "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 275 | "gio-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 276 | "glib 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", 277 | "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 278 | "gobject-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 279 | "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 280 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 281 | ] 282 | 283 | [[package]] 284 | name = "gio-sys" 285 | version = "0.9.1" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | dependencies = [ 288 | "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 289 | "gobject-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 290 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 291 | "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", 292 | ] 293 | 294 | [[package]] 295 | name = "glib" 296 | version = "0.9.3" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | dependencies = [ 299 | "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 300 | "futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 301 | "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 302 | "futures-executor 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 303 | "futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 304 | "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 305 | "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 306 | "gobject-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 307 | "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 308 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 309 | ] 310 | 311 | [[package]] 312 | name = "glib-sys" 313 | version = "0.9.1" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | dependencies = [ 316 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 317 | "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", 318 | ] 319 | 320 | [[package]] 321 | name = "gobject-sys" 322 | version = "0.9.1" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | dependencies = [ 325 | "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 326 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 327 | "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", 328 | ] 329 | 330 | [[package]] 331 | name = "gst-wpe-broadcast-demo" 332 | version = "1.0.0" 333 | dependencies = [ 334 | "base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", 335 | "cairo-rs 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", 336 | "gio 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", 337 | "glib 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", 338 | "gstreamer 0.15.4 (registry+https://github.com/rust-lang/crates.io-index)", 339 | "gtk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", 340 | "num 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 341 | "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", 342 | "serde_any 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", 343 | "strfmt 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 344 | ] 345 | 346 | [[package]] 347 | name = "gstreamer" 348 | version = "0.15.4" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | dependencies = [ 351 | "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 352 | "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", 353 | "futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 354 | "futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 355 | "futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 356 | "glib 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", 357 | "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 358 | "gobject-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 359 | "gstreamer-sys 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", 360 | "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 361 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 362 | "muldiv 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 363 | "num-rational 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", 364 | "paste 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", 365 | ] 366 | 367 | [[package]] 368 | name = "gstreamer-sys" 369 | version = "0.8.1" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | dependencies = [ 372 | "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 373 | "gobject-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 374 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 375 | "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", 376 | ] 377 | 378 | [[package]] 379 | name = "gtk" 380 | version = "0.8.1" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | dependencies = [ 383 | "atk 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", 384 | "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 385 | "cairo-rs 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", 386 | "cairo-sys-rs 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", 387 | "cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)", 388 | "gdk 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", 389 | "gdk-pixbuf 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", 390 | "gdk-pixbuf-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 391 | "gdk-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 392 | "gio 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", 393 | "gio-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 394 | "glib 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", 395 | "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 396 | "gobject-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 397 | "gtk-sys 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", 398 | "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 399 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 400 | "pango 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", 401 | "pango-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 402 | ] 403 | 404 | [[package]] 405 | name = "gtk-sys" 406 | version = "0.9.2" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | dependencies = [ 409 | "atk-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 410 | "cairo-sys-rs 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", 411 | "gdk-pixbuf-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 412 | "gdk-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 413 | "gio-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 414 | "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 415 | "gobject-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 416 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 417 | "pango-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 418 | "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", 419 | ] 420 | 421 | [[package]] 422 | name = "idna" 423 | version = "0.1.5" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | dependencies = [ 426 | "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", 427 | "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 428 | "unicode-normalization 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)", 429 | ] 430 | 431 | [[package]] 432 | name = "itoa" 433 | version = "0.4.5" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | 436 | [[package]] 437 | name = "lazy_static" 438 | version = "1.4.0" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | 441 | [[package]] 442 | name = "libc" 443 | version = "0.2.68" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | 446 | [[package]] 447 | name = "linked-hash-map" 448 | version = "0.5.2" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | 451 | [[package]] 452 | name = "log" 453 | version = "0.3.9" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | dependencies = [ 456 | "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", 457 | ] 458 | 459 | [[package]] 460 | name = "log" 461 | version = "0.4.8" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | dependencies = [ 464 | "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", 465 | ] 466 | 467 | [[package]] 468 | name = "matches" 469 | version = "0.1.8" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | 472 | [[package]] 473 | name = "muldiv" 474 | version = "0.2.1" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | 477 | [[package]] 478 | name = "num" 479 | version = "0.2.1" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | dependencies = [ 482 | "num-bigint 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", 483 | "num-complex 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", 484 | "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", 485 | "num-iter 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", 486 | "num-rational 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", 487 | "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 488 | ] 489 | 490 | [[package]] 491 | name = "num-bigint" 492 | version = "0.2.6" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | dependencies = [ 495 | "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 496 | "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", 497 | "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 498 | ] 499 | 500 | [[package]] 501 | name = "num-complex" 502 | version = "0.2.4" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | dependencies = [ 505 | "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 506 | "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 507 | ] 508 | 509 | [[package]] 510 | name = "num-integer" 511 | version = "0.1.42" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | dependencies = [ 514 | "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 515 | "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 516 | ] 517 | 518 | [[package]] 519 | name = "num-iter" 520 | version = "0.1.40" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | dependencies = [ 523 | "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 524 | "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", 525 | "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 526 | ] 527 | 528 | [[package]] 529 | name = "num-rational" 530 | version = "0.2.4" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | dependencies = [ 533 | "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 534 | "num-bigint 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", 535 | "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", 536 | "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 537 | ] 538 | 539 | [[package]] 540 | name = "num-traits" 541 | version = "0.2.11" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | dependencies = [ 544 | "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 545 | ] 546 | 547 | [[package]] 548 | name = "pango" 549 | version = "0.8.0" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | dependencies = [ 552 | "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 553 | "glib 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", 554 | "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 555 | "gobject-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 556 | "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 557 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 558 | "pango-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 559 | ] 560 | 561 | [[package]] 562 | name = "pango-sys" 563 | version = "0.9.1" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | dependencies = [ 566 | "glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 567 | "gobject-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 568 | "libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)", 569 | "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", 570 | ] 571 | 572 | [[package]] 573 | name = "paste" 574 | version = "0.1.10" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | dependencies = [ 577 | "paste-impl 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", 578 | "proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)", 579 | ] 580 | 581 | [[package]] 582 | name = "paste-impl" 583 | version = "0.1.10" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | dependencies = [ 586 | "proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)", 587 | "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", 588 | "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", 589 | "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", 590 | ] 591 | 592 | [[package]] 593 | name = "percent-encoding" 594 | version = "1.0.1" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | 597 | [[package]] 598 | name = "pin-utils" 599 | version = "0.1.0-alpha.4" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | 602 | [[package]] 603 | name = "pkg-config" 604 | version = "0.3.17" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | 607 | [[package]] 608 | name = "proc-macro-hack" 609 | version = "0.5.15" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | 612 | [[package]] 613 | name = "proc-macro-nested" 614 | version = "0.1.4" 615 | source = "registry+https://github.com/rust-lang/crates.io-index" 616 | 617 | [[package]] 618 | name = "proc-macro2" 619 | version = "1.0.10" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | dependencies = [ 622 | "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 623 | ] 624 | 625 | [[package]] 626 | name = "quote" 627 | version = "1.0.3" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | dependencies = [ 630 | "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", 631 | ] 632 | 633 | [[package]] 634 | name = "ron" 635 | version = "0.3.0" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | dependencies = [ 638 | "base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)", 639 | "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 640 | "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", 641 | ] 642 | 643 | [[package]] 644 | name = "rustc-demangle" 645 | version = "0.1.16" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | 648 | [[package]] 649 | name = "ryu" 650 | version = "1.0.3" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | 653 | [[package]] 654 | name = "safemem" 655 | version = "0.3.3" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | 658 | [[package]] 659 | name = "serde" 660 | version = "1.0.106" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | dependencies = [ 663 | "serde_derive 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", 664 | ] 665 | 666 | [[package]] 667 | name = "serde-xml-any" 668 | version = "0.0.3" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | dependencies = [ 671 | "error-chain 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", 672 | "log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", 673 | "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", 674 | "xml-rs 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)", 675 | ] 676 | 677 | [[package]] 678 | name = "serde_any" 679 | version = "0.5.0" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | dependencies = [ 682 | "failure 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 683 | "ron 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 684 | "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", 685 | "serde-xml-any 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", 686 | "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", 687 | "serde_urlencoded 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)", 688 | "serde_yaml 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)", 689 | "toml 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", 690 | ] 691 | 692 | [[package]] 693 | name = "serde_derive" 694 | version = "1.0.106" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | dependencies = [ 697 | "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", 698 | "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", 699 | "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", 700 | ] 701 | 702 | [[package]] 703 | name = "serde_json" 704 | version = "1.0.51" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | dependencies = [ 707 | "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", 708 | "ryu 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", 709 | "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", 710 | ] 711 | 712 | [[package]] 713 | name = "serde_urlencoded" 714 | version = "0.5.5" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | dependencies = [ 717 | "dtoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", 718 | "itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", 719 | "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", 720 | "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", 721 | ] 722 | 723 | [[package]] 724 | name = "serde_yaml" 725 | version = "0.7.5" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | dependencies = [ 728 | "dtoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", 729 | "linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", 730 | "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", 731 | "yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", 732 | ] 733 | 734 | [[package]] 735 | name = "slab" 736 | version = "0.4.2" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | 739 | [[package]] 740 | name = "smallvec" 741 | version = "1.3.0" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | 744 | [[package]] 745 | name = "strfmt" 746 | version = "0.1.6" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | 749 | [[package]] 750 | name = "syn" 751 | version = "1.0.17" 752 | source = "registry+https://github.com/rust-lang/crates.io-index" 753 | dependencies = [ 754 | "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", 755 | "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", 756 | "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 757 | ] 758 | 759 | [[package]] 760 | name = "synstructure" 761 | version = "0.12.3" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | dependencies = [ 764 | "proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", 765 | "quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", 766 | "syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)", 767 | "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 768 | ] 769 | 770 | [[package]] 771 | name = "toml" 772 | version = "0.4.10" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | dependencies = [ 775 | "serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)", 776 | ] 777 | 778 | [[package]] 779 | name = "unicode-bidi" 780 | version = "0.3.4" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | dependencies = [ 783 | "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", 784 | ] 785 | 786 | [[package]] 787 | name = "unicode-normalization" 788 | version = "0.1.12" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | dependencies = [ 791 | "smallvec 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 792 | ] 793 | 794 | [[package]] 795 | name = "unicode-xid" 796 | version = "0.2.0" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | 799 | [[package]] 800 | name = "url" 801 | version = "1.7.2" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | dependencies = [ 804 | "idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", 805 | "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", 806 | "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 807 | ] 808 | 809 | [[package]] 810 | name = "xml-rs" 811 | version = "0.6.1" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | dependencies = [ 814 | "bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 815 | ] 816 | 817 | [[package]] 818 | name = "yaml-rust" 819 | version = "0.4.3" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | dependencies = [ 822 | "linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", 823 | ] 824 | 825 | [metadata] 826 | "checksum atk 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "444daefa55f229af145ea58d77efd23725024ee1f6f3102743709aa6b18c663e" 827 | "checksum atk-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e552c1776737a4c80110d06b36d099f47c727335f9aaa5d942a72b6863a8ec6f" 828 | "checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" 829 | "checksum backtrace 0.3.46 (registry+https://github.com/rust-lang/crates.io-index)" = "b1e692897359247cc6bb902933361652380af0f1b7651ae5c5013407f30e109e" 830 | "checksum backtrace-sys 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "7de8aba10a69c8e8d7622c5710229485ec32e9d55fdad160ea559c086fdcd118" 831 | "checksum base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" 832 | "checksum base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" 833 | "checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" 834 | "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 835 | "checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" 836 | "checksum cairo-rs 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "157049ba9618aa3a61c39d5d785102c04d3b1f40632a706c621a9aedc21e6084" 837 | "checksum cairo-sys-rs 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ff65ba02cac715be836f63429ab00a767d48336efc5497c5637afb53b4f14d63" 838 | "checksum cc 1.0.50 (registry+https://github.com/rust-lang/crates.io-index)" = "95e28fa049fda1c330bcf9d723be7663a899c4679724b34c81e9f5a326aab8cd" 839 | "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 840 | "checksum dtoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3" 841 | "checksum error-chain 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d9435d864e017c3c6afeac1654189b06cdb491cf2ff73dbf0d73b0f292f42ff8" 842 | "checksum failure 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "b8529c2421efa3066a5cbd8063d2244603824daccb6936b079010bb2aa89464b" 843 | "checksum failure_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "030a733c8287d6213886dd487564ff5c8f6aae10278b3588ed177f9d18f8d231" 844 | "checksum futures-channel 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f0c77d04ce8edd9cb903932b608268b3fffec4163dc053b3b402bf47eac1f1a8" 845 | "checksum futures-core 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f25592f769825e89b92358db00d26f965761e094951ac44d3663ef25b7ac464a" 846 | "checksum futures-executor 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f674f3e1bcb15b37284a90cedf55afdba482ab061c407a9c0ebbd0f3109741ba" 847 | "checksum futures-io 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a638959aa96152c7a4cddf50fcb1e3fede0583b27157c26e67d6f99904090dc6" 848 | "checksum futures-macro 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "9a5081aa3de1f7542a794a397cde100ed903b0630152d0973479018fd85423a7" 849 | "checksum futures-task 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7b0a34e53cf6cdcd0178aa573aed466b646eb3db769570841fda0c7ede375a27" 850 | "checksum futures-util 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "22766cf25d64306bedf0384da004d05c9974ab104fcc4528f1236181c18004c5" 851 | "checksum gdk 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "fbe5e8772fc0865c52460cdd7a59d7d47700f44d9809d1dd00eecceb769a7589" 852 | "checksum gdk-pixbuf 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e248220c46b329b097d4b158d2717f8c688f16dd76d0399ace82b3e98062bdd7" 853 | "checksum gdk-pixbuf-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d8991b060a9e9161bafd09bf4a202e6fd404f5b4dd1a08d53a1e84256fb34ab0" 854 | "checksum gdk-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6adf679e91d1bff0c06860287f80403e7db54c2d2424dce0a470023b56c88fbb" 855 | "checksum gio 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0cd10f9415cce39b53f8024bf39a21f84f8157afa52da53837b102e585a296a5" 856 | "checksum gio-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4fad225242b9eae7ec8a063bb86974aca56885014672375e5775dc0ea3533911" 857 | "checksum glib 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "40fb573a09841b6386ddf15fd4bc6655b4f5b106ca962f57ecaecde32a0061c0" 858 | "checksum glib-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "95856f3802f446c05feffa5e24859fe6a183a7cb849c8449afc35c86b1e316e2" 859 | "checksum gobject-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31d1a804f62034eccf370006ccaef3708a71c31d561fee88564abe71177553d9" 860 | "checksum gstreamer 0.15.4 (registry+https://github.com/rust-lang/crates.io-index)" = "d90b963bf1778afbe23d694ab3c4d24a21ed2e65e0ac7ba86ea26ecab93b9811" 861 | "checksum gstreamer-sys 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1d18da01b97d0ab5896acd5151e4c155acefd0e6c03c3dd24dd133ba054053db" 862 | "checksum gtk 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "87e1e8d70290239c668594002d1b174fcc7d7ef5d26670ee141490ede8facf8f" 863 | "checksum gtk-sys 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "53def660c7b48b00b510c81ef2d2fbd3c570f1527081d8d7947f471513e1a4c1" 864 | "checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" 865 | "checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" 866 | "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 867 | "checksum libc 0.2.68 (registry+https://github.com/rust-lang/crates.io-index)" = "dea0c0405123bba743ee3f91f49b1c7cfb684eef0da0a50110f758ccf24cdff0" 868 | "checksum linked-hash-map 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ae91b68aebc4ddb91978b11a1b02ddd8602a05ec19002801c5666000e05e0f83" 869 | "checksum log 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" 870 | "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 871 | "checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" 872 | "checksum muldiv 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204" 873 | "checksum num 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" 874 | "checksum num-bigint 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" 875 | "checksum num-complex 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" 876 | "checksum num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" 877 | "checksum num-iter 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "dfb0800a0291891dd9f4fe7bd9c19384f98f7fbe0cd0f39a2c6b88b9868bbc00" 878 | "checksum num-rational 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" 879 | "checksum num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" 880 | "checksum pango 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1e9c6b728f1be8edb5f9f981420b651d5ea30bdb9de89f1f1262d0084a020577" 881 | "checksum pango-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "86b93d84907b3cf0819bff8f13598ba72843bee579d5ebc2502e4b0367b4be7d" 882 | "checksum paste 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "ab4fb1930692d1b6a9cfabdde3d06ea0a7d186518e2f4d67660d8970e2fa647a" 883 | "checksum paste-impl 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "a62486e111e571b1e93b710b61e8f493c0013be39629b714cb166bdb06aa5a8a" 884 | "checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" 885 | "checksum pin-utils 0.1.0-alpha.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5894c618ce612a3fa23881b152b608bafb8c56cfc22f434a3ba3120b40f7b587" 886 | "checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" 887 | "checksum proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)" = "0d659fe7c6d27f25e9d80a1a094c223f5246f6a6596453e09d7229bf42750b63" 888 | "checksum proc-macro-nested 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8e946095f9d3ed29ec38de908c22f95d9ac008e424c7bcae54c75a79c527c694" 889 | "checksum proc-macro2 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)" = "df246d292ff63439fea9bc8c0a270bed0e390d5ebd4db4ba15aba81111b5abe3" 890 | "checksum quote 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2bdc6c187c65bca4260c9011c9e3132efe4909da44726bad24cf7572ae338d7f" 891 | "checksum ron 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a9fa11b7a38511d46ff1959ae46ebb60bd8a746f17bdd0206b4c8de7559ac47b" 892 | "checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" 893 | "checksum ryu 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535622e6be132bccd223f4bb2b8ac8d53cda3c7a6394944d3b2b33fb974f9d76" 894 | "checksum safemem 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" 895 | "checksum serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)" = "36df6ac6412072f67cf767ebbde4133a5b2e88e76dc6187fa7104cd16f783399" 896 | "checksum serde-xml-any 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e281928a3e3104e809dbd19b78cef7ef7c29662cf2583a94c032485aa2e7586b" 897 | "checksum serde_any 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "38cb506febacc2cf6533279947bd37b69ce91782af1aedf31c7e6181a77d46ee" 898 | "checksum serde_derive 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)" = "9e549e3abf4fb8621bd1609f11dfc9f5e50320802273b12f3811a67e6716ea6c" 899 | "checksum serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)" = "da07b57ee2623368351e9a0488bb0b261322a15a6e0ae53e243cbdc0f4208da9" 900 | "checksum serde_urlencoded 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "642dd69105886af2efd227f75a520ec9b44a820d65bc133a9131f7d229fd165a" 901 | "checksum serde_yaml 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ef8099d3df28273c99a1728190c7a9f19d444c941044f64adf986bee7ec53051" 902 | "checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" 903 | "checksum smallvec 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05720e22615919e4734f6a99ceae50d00226c3c5aca406e102ebc33298214e0a" 904 | "checksum strfmt 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "b278b244ef7aa5852b277f52dd0c6cac3a109919e1f6d699adde63251227a30f" 905 | "checksum syn 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "0df0eb663f387145cab623dea85b09c2c5b4b0aef44e945d928e682fce71bb03" 906 | "checksum synstructure 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67656ea1dc1b41b1451851562ea232ec2e5a80242139f7e679ceccfb5d61f545" 907 | "checksum toml 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "758664fc71a3a69038656bee8b6be6477d2a6c315a6b81f7081f591bffa4111f" 908 | "checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" 909 | "checksum unicode-normalization 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4" 910 | "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 911 | "checksum url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" 912 | "checksum xml-rs 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e1945e12e16b951721d7976520b0832496ef79c31602c7a29d950de79ba74621" 913 | "checksum yaml-rust 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "65923dd1784f44da1d2c3dbbc5e822045628c590ba72123e1c73d3c230c4434d" 914 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gst-wpe-broadcast-demo" 3 | version = "1.0.0" 4 | authors = ["Philippe Normand "] 5 | license = "MIT" 6 | edition = "2018" 7 | 8 | [dependencies] 9 | glib = "0.9" 10 | gio = "0.8" 11 | gtk = "0.8" 12 | gst = { package = "gstreamer", version = "0.15", features = ["v1_10"] } 13 | serde = "1.0" 14 | serde_any = "0.5" 15 | strfmt = "0.1.6" 16 | base64 = "0.11" 17 | cairo-rs = "0.8" 18 | num = "0.2" 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Sebastian Dröge, Guillaume Gomez, Jordan Petridis 4 | Copyright (c) 2019 Sebastian Dröge 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GStreamer WPE Web Overlay Broadcast demo 2 | 3 | This application allows the live video input (webcam) to be mixed with the 4 | contents of a web page and streamed to an RTMP end-point (Twitch, Youtube,...). 5 | 6 | ![Screenshot](data/screenshot.png) 7 | 8 | ## Installation 9 | 10 | The application is packaged as a Flatpak app, currently hosted on Igalia's 11 | software server. To install it run this on your Linux desktop terminal: 12 | 13 | ```shell 14 | $ flatpak install --user https://software.igalia.com/flatpak-refs/gst-wpe-broadcast-demo.flatpakref 15 | ``` 16 | 17 | The recommended way to run it is also from the terminal because sometimes the UI 18 | hangs up and needs a restart: 19 | 20 | ```shell 21 | $ flatpak run --user com.igalia.GstWPEBroadcastDemo 22 | ``` 23 | 24 | ## Live stream setup 25 | 26 | The first time you press the 'record' button the application will ask you to 27 | configure the RTMP URL. It can be done via the settings window, accessible from 28 | the Hamburger menu on top-right corner of the main window. You need to provide 29 | an URL, it differs depending on where you want to live-stream. 30 | 31 | ### Twitch 32 | 33 | In your Twitch channel settings, retrieve the Primary Stream key and copy it in 34 | the pasteboard. In the demo settings, paste it with the following prefix in 35 | order to build a valid RTMP URL: `rtmp://live.twitch.tv/app/` 36 | 37 | Users can access the stream using `https://twitch.tv/yourusername`. 38 | 39 | ### Youtube 40 | 41 | In your [Live Dashboard](https://www.youtube.com/live_dashboard) scroll down to 42 | the encoder setup section, reveal the stream key and copy it in pasteboard. In 43 | the following URL, replace `STREAM_KEY` with your secret stream key to build a 44 | valid RTMP end-point URL: `rtmp://a.rtmp.youtube.com/live2/x/STREAM_KEY app=live2`. 45 | The space before `app` is important, don't remove it. 46 | 47 | For every new live stream a new Youtube video is created. This is not very 48 | convenient for us, you need to find the URL via the dashboard, on the video 49 | preview, right click and copy the video URL, then share it to the users (booth 50 | visitors). 51 | 52 | ## Release procedure 53 | 54 | - Bump version in `Cargo.toml` and `meson.build` 55 | - Add release info in appstream file 56 | - Commit and tag new version: 57 | 58 | $ git ci -am "Bump to ..." 59 | $ git tag -s "version..." 60 | 61 | - Build tarball: 62 | 63 | $ mkdir -p _build 64 | $ cargo install cargo-vendor 65 | $ pip3 install --user -U meson 66 | $ meson _build 67 | $ ninja -C _build release 68 | 69 | - Publish version and tag: 70 | 71 | $ git push --tags 72 | $ git push 73 | 74 | - Upload tarball from `_build/dist/` to Gitlab 75 | 76 | 77 | ## Credits 78 | 79 | The code is based on the [GUADEC 2019 Rust/GTK/GStreamer workshop app](https://gitlab.gnome.org/sdroege/guadec-workshop-2019). Many thanks to Sebastian Dröge and Guillaume Gomez 80 | ! 81 | 82 | The HTML/CSS template is based on the [Pure CSS Horizontal Ticker codepen](https://codepen.io/lewismcarey/pen/GJZVoG). 83 | -------------------------------------------------------------------------------- /data/com.igalia.GstWPEBroadcastDemo.appdata.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.igalia.GstWPEBroadcastDemo 5 | com.igalia.GstWPEBroadcastDemo 6 | CC-BY-3.0 7 | MIT 8 | gst-wpe-broadcast-demo 9 | GstWPEBroadcastDemo 10 | Live-streams your webcam with HTML overlays 11 | 12 |

13 | The GstWPEBroadcast demo application allows the live video input (webcam) to 14 | be mixed with the contents of a web page and streamed to an RTMP 15 | end-point (Twitch, Youtube,...) which 16 | can be configured in the settings. 17 |

18 |
19 | philn_AT_igalia.com 20 | Philippe Normand 21 | https://github.com/igalia/gst-wpe-broadcast-demo 22 | 23 | 24 | https://github.com/igalia/gst-wpe-broadcast-demo/raw/master/data/screenshot.png 25 | Screenshot 26 | 27 | 28 | 29 | 30 | 31 |

Initial release of the GstWPE broadcast demo.

32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /data/com.igalia.GstWPEBroadcastDemo.desktop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env xdg-open 2 | 3 | [Desktop Entry] 4 | Version=1.0 5 | Name[en_GB]=GstWPEBroadcastDemo 6 | Name=GstWPEBroadcastDemo 7 | Comment[en_GB]=Live-streams your webcam with HTML overlays 8 | Comment=Live-streams your webcam with HTML overlays 9 | Keywords[en_GB]=Audio;Video; 10 | Keywords=Audio;Video; 11 | Exec=gst-wpe-broadcast-demo 12 | DBusActivatable=false 13 | Terminal=false 14 | Type=Application 15 | Categories=GTK;GNOME;AudioVideo;Audio;Video; 16 | StartupNotify=true 17 | Icon=com.igalia.GstWPEBroadcastDemo.svg -------------------------------------------------------------------------------- /data/com.igalia.GstWPEBroadcastDemo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 | 26 | 29 | 33 | 37 | 38 | 40 | 44 | 48 | 49 | 51 | 55 | 59 | 63 | 64 | 67 | 71 | 75 | 76 | 79 | 83 | 87 | 88 | 91 | 95 | 99 | 100 | 103 | 107 | 111 | 112 | 115 | 119 | 123 | 124 | 127 | 131 | 135 | 136 | 139 | 143 | 147 | 148 | 150 | 154 | 158 | 162 | 166 | 167 | 177 | 188 | 197 | 206 | 215 | 226 | 229 | 233 | 237 | 238 | 240 | 244 | 248 | 252 | 253 | 256 | 260 | 264 | 265 | 274 | 285 | 296 | 305 | 314 | 321 | 325 | 326 | 337 | 348 | 355 | 359 | 360 | 369 | 370 | 391 | 393 | 394 | 396 | image/svg+xml 397 | 399 | 401 | Webcam icon 402 | 403 | 404 | Josef Vybíral, Vinicius Depizzol 405 | 406 | 407 | 408 | 409 | Jakub Steiner 410 | 411 | 412 | 413 | 414 | webcam 415 | video 416 | conference 417 | camera 418 | camera web 419 | 420 | 421 | 422 | 424 | 426 | 428 | 430 | 432 | 434 | 436 | 437 | 438 | 439 | 443 | 453 | 458 | 463 | 473 | 478 | 482 | 492 | 502 | 506 | 516 | 526 | 531 | 535 | 540 | 550 | 560 | 570 | 580 | 590 | 600 | 610 | 620 | 630 | 631 | 632 | -------------------------------------------------------------------------------- /data/gst-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | ]> 7 | 9 | 14 | 18 | 22 | 24 | 26 | 29 | 34 | 37 | 45 | 48 | 51 | 54 | 55 | -------------------------------------------------------------------------------- /data/igalia-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Igalia/gst-wpe-broadcast-demo/7b7b60463eb623fa7d799c801623c5690d8e3f30/data/igalia-logo.png -------------------------------------------------------------------------------- /data/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 |
12 |

HTML Video overlay in GStreamer with WPEWebKit

13 |

The rolling news ticker at the bottom of the screen is styled and animated with CSS

14 |
15 |
16 | 17 | 18 |
19 |
20 |
21 |
Audio support coming soon to GstWPE!
22 |
GStreamer 1.16.1 has been released!
23 |
This demo was show-cased at the GStreamer conference
24 |
and at ELC-E! Both took place in Lyon, France
25 |
26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /data/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Igalia/gst-wpe-broadcast-demo/7b7b60463eb623fa7d799c801623c5690d8e3f30/data/screenshot.png -------------------------------------------------------------------------------- /data/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | .header { 6 | background-color: rgba(255, 255, 255, 0.5); 7 | } 8 | 9 | .logos { 10 | vertical-align: bottom; 11 | } 12 | 13 | #igalia-logo { 14 | position: absolute; 15 | right: 0px; 16 | padding: 10px; 17 | } 18 | 19 | #gst-logo { 20 | position: absolute; 21 | left: 0px; 22 | padding: 10px; 23 | } 24 | 25 | @-webkit-keyframes ticker { 26 | 0% { 27 | -webkit-transform: translate3d(0, 0, 0); 28 | transform: translate3d(0, 0, 0); 29 | visibility: visible; 30 | } 31 | 100% { 32 | -webkit-transform: translate3d(-100%, 0, 0); 33 | transform: translate3d(-100%, 0, 0); 34 | } 35 | } 36 | @keyframes ticker { 37 | 0% { 38 | -webkit-transform: translate3d(0, 0, 0); 39 | transform: translate3d(0, 0, 0); 40 | visibility: visible; 41 | } 42 | 100% { 43 | -webkit-transform: translate3d(-100%, 0, 0); 44 | transform: translate3d(-100%, 0, 0); 45 | } 46 | } 47 | .ticker-wrap { 48 | position: fixed; 49 | bottom: 0; 50 | width: 100%; 51 | overflow: hidden; 52 | height: 4rem; 53 | background-color: rgba(0, 0, 0, 0.9); 54 | padding-left: 100%; 55 | box-sizing: content-box; 56 | } 57 | .ticker-wrap .ticker { 58 | display: inline-block; 59 | height: 4rem; 60 | line-height: 4rem; 61 | white-space: nowrap; 62 | padding-right: 100%; 63 | box-sizing: content-box; 64 | -webkit-animation-iteration-count: infinite; 65 | animation-iteration-count: infinite; 66 | -webkit-animation-timing-function: linear; 67 | animation-timing-function: linear; 68 | -webkit-animation-name: ticker; 69 | animation-name: ticker; 70 | -webkit-animation-duration: 30s; 71 | animation-duration: 30s; 72 | } 73 | .ticker-wrap .ticker__item { 74 | display: inline-block; 75 | padding: 0 2rem; 76 | font-size: 2rem; 77 | color: white; 78 | } 79 | 80 | body { 81 | padding-bottom: 5rem; 82 | } 83 | 84 | h1, h2, p { 85 | padding: 0 5%; 86 | } 87 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'gst-wpe-broadcast-demo', 'rust', 3 | version: '1.0.0', 4 | license: 'MIT', 5 | ) 6 | 7 | gst_wpe_broadcast_demo_version = meson.project_version() 8 | version_array = gst_wpe_broadcast_demo_version.split('.') 9 | gst_wpe_broadcast_demo_major_version = version_array[0].to_int() 10 | gst_wpe_broadcast_demo_minor_version = version_array[1].to_int() 11 | gst_wpe_broadcast_demo_version_micro = version_array[2].to_int() 12 | 13 | gst_wpe_broadcast_demo_prefix = get_option('prefix') 14 | gst_wpe_broadcast_demo_bindir = join_paths(gst_wpe_broadcast_demo_prefix, get_option('bindir')) 15 | gst_wpe_broadcast_demo_localedir = join_paths(gst_wpe_broadcast_demo_prefix, get_option('localedir')) 16 | 17 | datadir = get_option('datadir') 18 | icondir = join_paths(datadir, 'icons') 19 | 20 | cargo = find_program('cargo') 21 | cargo_vendor = find_program('cargo-vendor', required: false) 22 | cargo_script = find_program('scripts/cargo.sh') 23 | grabber = find_program('scripts/grabber.sh') 24 | cargo_release = find_program('scripts/release.sh') 25 | 26 | c = run_command(grabber) 27 | sources = c.stdout().strip().split('\n') 28 | 29 | install_data('data/com.igalia.GstWPEBroadcastDemo.desktop', install_dir : datadir + '/applications') 30 | install_data('data/com.igalia.GstWPEBroadcastDemo.svg', install_dir : icondir + '/hicolor/scalable/apps/') 31 | install_data('data/com.igalia.GstWPEBroadcastDemo.appdata.xml', install_dir : datadir + '/appdata/') 32 | 33 | cargo_release = custom_target('cargo-build', 34 | build_by_default: true, 35 | console: true, 36 | input: sources, 37 | output: ['gst-wpe-broadcast-demo'], 38 | install: true, 39 | install_dir: gst_wpe_broadcast_demo_bindir, 40 | command: [cargo_script, '@CURRENT_SOURCE_DIR@', '@OUTPUT@', gst_wpe_broadcast_demo_localedir]) 41 | 42 | run_target('release', command: ['scripts/release.sh', 43 | meson.project_name() + '-' + gst_wpe_broadcast_demo_version 44 | ]) 45 | 46 | meson.add_install_script('scripts/meson_post_install.py') 47 | -------------------------------------------------------------------------------- /scripts/cargo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export CARGO_HOME=$1/target/cargo-home 4 | 5 | if [[ $DEBUG = true ]] 6 | then 7 | echo "DEBUG MODE" 8 | cargo build --manifest-path $1/Cargo.toml -p gst-wpe-broadcast-demo && cp $1/target/debug/gst-wpe-broadcast-demo $2 9 | else 10 | echo "RELEASE MODE" 11 | cargo build --manifest-path $1/Cargo.toml --release -p gst-wpe-broadcast-demo && cp $1/target/release/gst-wpe-broadcast-demo $2 12 | fi 13 | -------------------------------------------------------------------------------- /scripts/grabber.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | find src/ -name *.rs 4 | -------------------------------------------------------------------------------- /scripts/meson_post_install.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import subprocess 5 | 6 | install_prefix = os.environ['MESON_INSTALL_PREFIX'] 7 | icondir = os.path.join(install_prefix, 'share', 'icons', 'hicolor') 8 | schemadir = os.path.join(install_prefix, 'share', 'glib-2.0', 'schemas') 9 | 10 | if not os.environ.get('DESTDIR'): 11 | print('Update icon cache...') 12 | subprocess.call(['gtk-update-icon-cache', '-f', '-t', icondir]) 13 | 14 | print('Compiling gsettings schemas...') 15 | subprocess.call(['glib-compile-schemas', schemadir]) 16 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | VERSION=$1 4 | DEST=${MESON_BUILD_ROOT} 5 | DIST=$DEST/dist/$VERSION 6 | 7 | 8 | cd "${MESON_SOURCE_ROOT}" 9 | mkdir -p $DIST 10 | 11 | # copying files 12 | rsync -rR $(git ls-files) $DIST 13 | 14 | # cargo vendor 15 | mkdir $DIST/.cargo 16 | cargo vendor | sed 's/^directory = ".*"/directory = "vendor"/g' > $DIST/.cargo/config 17 | cp -rf vendor $DIST/ 18 | 19 | # packaging 20 | cd $DEST/dist 21 | tar -cJvf $VERSION.tar.xz $VERSION 22 | -------------------------------------------------------------------------------- /src/about_dialog.rs: -------------------------------------------------------------------------------- 1 | use gtk::{self, prelude::*}; 2 | 3 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 4 | 5 | pub fn show_about_dialog(application: >k::Application) { 6 | let dialog = gtk::AboutDialog::new(); 7 | 8 | dialog.set_authors(&["Philippe Normand"]); 9 | dialog.set_website_label(Some("Github repository")); 10 | dialog.set_website(Some("https://github.com/igalia/gst-wpe-broadcast-demo/")); 11 | dialog.set_comments(Some( 12 | "WebCam and Web-page inputs mixed together and streamed to an RTMP end-point.\n \ 13 | \n \ 14 | The code is based on the GUADEC 2019 Rust/GTK/GStreamer workshop app (https://gitlab.gnome.org/sdroege/guadec-workshop-2019). Many thanks to Sebastian Dröge and Guillaume Gomez . \n \ 15 | \n \ 16 | The HTML/CSS template is based on the Pure CSS Horizontal Ticker codepen: https://codepen.io/lewismcarey/pen/GJZVoG." 17 | )); 18 | dialog.set_copyright(Some("Licensed under MIT license")); 19 | dialog.set_program_name("GStreamer WPE Broadcast demo"); 20 | dialog.set_logo_icon_name(Some("camera-web")); 21 | dialog.set_version(Some(VERSION)); 22 | 23 | // Make the about dialog modal and transient for our currently active application window. This 24 | // prevents the user from sending any events to the main window as long as the dialog is open. 25 | dialog.set_transient_for(application.get_active_window().as_ref()); 26 | dialog.set_modal(true); 27 | 28 | // When any response on the dialog happens, we simply destroy it. 29 | // 30 | // We don't have any custom buttons added so this will only ever handle the close button. 31 | // Otherwise we could distinguish the buttons by the response 32 | dialog.connect_response(|dialog, _response| { 33 | dialog.destroy(); 34 | }); 35 | 36 | dialog.show_all(); 37 | } 38 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use gio::{self, prelude::*}; 2 | use glib; 3 | use gtk::{self, prelude::*}; 4 | 5 | use crate::about_dialog::show_about_dialog; 6 | use crate::audio_vumeter; 7 | use crate::header_bar::HeaderBar; 8 | use crate::pipeline::Pipeline; 9 | use crate::settings::show_settings_dialog; 10 | use crate::utils; 11 | 12 | use std::cell::RefCell; 13 | use std::error; 14 | use std::ops; 15 | use std::rc::{Rc, Weak}; 16 | 17 | // Our refcounted application struct for containing all the state we have to carry around. 18 | // 19 | // This represents our main application window. 20 | #[derive(Clone)] 21 | pub struct App(Rc); 22 | 23 | // Deref into the contained struct to make usage a bit more ergonomic 24 | impl ops::Deref for App { 25 | type Target = AppInner; 26 | 27 | fn deref(&self) -> &AppInner { 28 | &*self.0 29 | } 30 | } 31 | 32 | // Weak reference to our application struct 33 | // 34 | // Weak references are important to prevent reference cycles. Reference cycles are cases where 35 | // struct A references directly or indirectly struct B, and struct B references struct A again 36 | // while both are using reference counting. 37 | pub struct AppWeak(Weak); 38 | 39 | impl AppWeak { 40 | // Upgrade to a strong reference if it still exists 41 | pub fn upgrade(&self) -> Option { 42 | self.0.upgrade().map(App) 43 | } 44 | } 45 | 46 | pub struct AppInner { 47 | main_window: gtk::ApplicationWindow, 48 | header_bar: HeaderBar, 49 | pipeline: Pipeline, 50 | text_view: gtk::TextView, 51 | css_buffer: RefCell, 52 | html_buffer: RefCell, 53 | editing_markup: RefCell>, 54 | #[allow(dead_code)] 55 | audio_vumeter: audio_vumeter::AudioVuMeter, 56 | } 57 | 58 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 59 | pub enum RecordState { 60 | Idle, 61 | Recording, 62 | } 63 | 64 | impl<'a> From<&'a glib::Variant> for RecordState { 65 | fn from(v: &glib::Variant) -> RecordState { 66 | v.get::().expect("Invalid record state type").into() 67 | } 68 | } 69 | 70 | impl From for RecordState { 71 | fn from(v: bool) -> RecordState { 72 | if v { 73 | RecordState::Recording 74 | } else { 75 | RecordState::Idle 76 | } 77 | } 78 | } 79 | 80 | impl From for glib::Variant { 81 | fn from(v: RecordState) -> glib::Variant { 82 | match v { 83 | RecordState::Idle => false.to_variant(), 84 | RecordState::Recording => true.to_variant(), 85 | } 86 | } 87 | } 88 | 89 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 90 | pub enum Action { 91 | Quit, 92 | Settings, 93 | About, 94 | Record(RecordState), 95 | #[allow(dead_code)] 96 | UpdateOverlay, 97 | } 98 | 99 | impl App { 100 | fn new(application: >k::Application) -> Result> { 101 | // Here build the UI but don't show it yet 102 | let window = gtk::ApplicationWindow::new(application); 103 | 104 | window.set_title("WebCam Viewer"); 105 | window.set_border_width(5); 106 | window.set_position(gtk::WindowPosition::Center); 107 | window.set_default_size(1200, -1); 108 | 109 | // Create headerbar for the application window 110 | let header_bar = HeaderBar::new(&window); 111 | 112 | let vumeter = audio_vumeter::AudioVuMeter::new(); 113 | 114 | // Create the pipeline and if that fail return 115 | let pipeline = Pipeline::new(vumeter.downgrade()) 116 | .map_err(|err| format!("Error creating pipeline: {:?}", err))?; 117 | 118 | let text_view = gtk::TextView::new(); 119 | text_view.set_size_request(400, 300); 120 | 121 | let scrolled_window = gtk::ScrolledWindow::new(gtk::NONE_ADJUSTMENT, gtk::NONE_ADJUSTMENT); 122 | scrolled_window.set_size_request(400, 300); 123 | scrolled_window.add(&text_view); 124 | 125 | let css_buffer = RefCell::new(include_str!("../data/style.css").to_string()); 126 | let html_buffer = RefCell::new(include_str!("../data/index.html").to_string()); 127 | 128 | let menu = gtk::ComboBoxText::new(); 129 | 130 | menu.append_text("CSS"); 131 | menu.append_text("HTML"); 132 | 133 | let update_button = gtk::Button::new_with_label("Update web-page overlay"); 134 | update_button 135 | .clone() 136 | .upcast::() 137 | .set_action_name(Some("app.update_overlay")); 138 | 139 | let vumeter_widget = vumeter.get_widget(); 140 | vumeter_widget.set_size_request(30, -1); 141 | 142 | let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 0); 143 | hbox.pack_start(&pipeline.get_widget(), false, false, 0); 144 | hbox.pack_start(vumeter_widget, false, false, 0); 145 | 146 | let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); 147 | vbox.pack_start(&menu, false, false, 0); 148 | vbox.pack_start(&scrolled_window, true, true, 0); 149 | vbox.pack_start(&update_button, false, false, 0); 150 | 151 | let paned = gtk::Paned::new(gtk::Orientation::Horizontal); 152 | paned.pack1(&hbox, false, false); 153 | paned.pack2(&vbox, false, false); 154 | paned.set_position(700); 155 | 156 | window.add(&paned); 157 | 158 | let app = App(Rc::new(AppInner { 159 | main_window: window, 160 | header_bar, 161 | pipeline, 162 | text_view, 163 | css_buffer, 164 | html_buffer, 165 | audio_vumeter: vumeter, 166 | editing_markup: RefCell::new(None), 167 | })); 168 | 169 | // Create the application actions 170 | Action::create(&app, &application); 171 | 172 | let weak_app = app.downgrade(); 173 | menu.connect_changed(move |widget| { 174 | let app = upgrade_weak!(weak_app); 175 | if let Some(selection) = widget.get_active_text() { 176 | if let Some(buffer) = app.text_view.get_buffer() { 177 | if selection == "CSS" { 178 | buffer.set_text(&*app.css_buffer.borrow()); 179 | } else { 180 | buffer.set_text(&*app.html_buffer.borrow()); 181 | } 182 | app.editing_markup.replace(Some(selection.to_string())); 183 | } 184 | } 185 | }); 186 | 187 | menu.set_active(Some(1)); 188 | 189 | Ok(app) 190 | } 191 | 192 | // Downgrade to a weak reference 193 | pub fn downgrade(&self) -> AppWeak { 194 | AppWeak(Rc::downgrade(&self.0)) 195 | } 196 | 197 | pub fn on_startup(application: >k::Application) { 198 | // Create application and error out if that fails for whatever reason 199 | let app = match App::new(application) { 200 | Ok(app) => app, 201 | Err(err) => { 202 | utils::show_error_dialog( 203 | true, 204 | format!("Error creating application: {}", err).as_str(), 205 | ); 206 | return; 207 | } 208 | }; 209 | 210 | // When the application is activated show the UI. This happens when the first process is 211 | // started, and in the first process whenever a second process is started 212 | let app_weak = app.downgrade(); 213 | application.connect_activate(move |_| { 214 | let app = upgrade_weak!(app_weak); 215 | app.on_activate(); 216 | }); 217 | 218 | // When the application is shut down we drop our app struct 219 | // 220 | // It has to be stored in a RefCell> to be able to pass it to a Fn closure. With 221 | // FnOnce this wouldn't be needed and the closure will only be called once, but the 222 | // bindings define all signal handlers as Fn. 223 | let app_container = RefCell::new(Some(app)); 224 | application.connect_shutdown(move |_| { 225 | let app = app_container 226 | .borrow_mut() 227 | .take() 228 | .expect("Shutdown called multiple times"); 229 | app.on_shutdown(); 230 | }); 231 | } 232 | 233 | // Called on the first application instance whenever the first application instance is started, 234 | // or any future second application instance 235 | fn on_activate(&self) { 236 | // Show our window and bring it to the foreground 237 | self.main_window.show_all(); 238 | 239 | // Have to call this instead of present() because of 240 | // https://gitlab.gnome.org/GNOME/gtk/issues/624 241 | self.main_window 242 | .present_with_time((glib::get_monotonic_time() / 1000) as u32); 243 | 244 | // Once the UI is shown, start the GStreamer pipeline. If 245 | // an error happens, we immediately shut down 246 | if let Err(err) = self.pipeline.start() { 247 | utils::show_error_dialog( 248 | true, 249 | format!("Failed to set pipeline to playing: {}", err).as_str(), 250 | ); 251 | } 252 | } 253 | 254 | // Called when the application shuts down. We drop our app struct here 255 | fn on_shutdown(self) { 256 | // This might fail but as we shut down right now anyway this doesn't matter 257 | // TODO: If a recording is currently running we would like to finish that first 258 | // before quitting the pipeline and shutting down the pipeline. 259 | let _ = self.pipeline.stop(); 260 | } 261 | 262 | // When the record button is clicked it triggers the record action, which will call this. 263 | // We have to start or stop recording here 264 | fn on_record_state_changed(&self, new_state: RecordState) { 265 | // Start/stop recording based on button active'ness 266 | match new_state { 267 | RecordState::Recording => { 268 | if let Err(err) = self.pipeline.start_recording() { 269 | utils::show_error_dialog( 270 | false, 271 | format!("Failed to start recording: {}", err).as_str(), 272 | ); 273 | self.header_bar.set_record_active(false); 274 | } 275 | } 276 | RecordState::Idle => self.pipeline.stop_recording(), 277 | } 278 | } 279 | 280 | fn update_overlay(&mut self) { 281 | if let Some(buffer) = self.text_view.get_buffer() { 282 | if let Some(data) = 283 | buffer.get_text(&buffer.get_start_iter(), &buffer.get_end_iter(), false) 284 | { 285 | if let Some(editing_markup) = &*self.editing_markup.borrow() { 286 | if editing_markup == "CSS" { 287 | self.css_buffer.replace(data.to_string()); 288 | } else { 289 | self.html_buffer.replace(data.to_string()); 290 | } 291 | } 292 | } 293 | } 294 | self.pipeline 295 | .update_overlay(&self.html_buffer.borrow(), &self.css_buffer.borrow()); 296 | } 297 | 298 | pub fn refresh_pipeline(&self) { 299 | self.pipeline.refresh(); 300 | } 301 | } 302 | 303 | impl Action { 304 | // The full action name as is used in e.g. menu models 305 | pub fn full_name(self) -> &'static str { 306 | match self { 307 | Action::Quit => "app.quit", 308 | Action::Settings => "app.settings", 309 | Action::About => "app.about", 310 | Action::Record(_) => "app.record", 311 | Action::UpdateOverlay => "app.update_overlay", 312 | } 313 | } 314 | 315 | // Create our application actions here 316 | // 317 | // These are connected to our buttons and can be triggered by the buttons, as well as remotely 318 | fn create(app: &App, application: >k::Application) { 319 | // settings action: when activated, show a settings dialog 320 | let settings = gio::SimpleAction::new("settings", None); 321 | let weak_application = application.downgrade(); 322 | let weak_app = app.downgrade(); 323 | settings.connect_activate(move |_action, _parameter| { 324 | let application = upgrade_weak!(weak_application); 325 | let app = upgrade_weak!(weak_app); 326 | 327 | show_settings_dialog(&application, &app); 328 | }); 329 | application.add_action(&settings); 330 | 331 | // about action: when activated it will show an about dialog 332 | let about = gio::SimpleAction::new("about", None); 333 | let weak_application = application.downgrade(); 334 | about.connect_activate(move |_action, _parameter| { 335 | let application = upgrade_weak!(weak_application); 336 | show_about_dialog(&application); 337 | }); 338 | application.add_action(&about); 339 | 340 | // When activated, shuts down the application 341 | let quit = gio::SimpleAction::new("quit", None); 342 | let weak_application = application.downgrade(); 343 | quit.connect_activate(move |_action, _parameter| { 344 | let application = upgrade_weak!(weak_application); 345 | application.quit(); 346 | }); 347 | application.add_action(&quit); 348 | 349 | // And add an accelerator for triggering the action on ctrl+q 350 | application.set_accels_for_action(Action::Quit.full_name(), &["Q"]); 351 | 352 | // record action: changes state between true/false 353 | let record = gio::SimpleAction::new_stateful("record", None, &RecordState::Idle.into()); 354 | let weak_app = app.downgrade(); 355 | record.connect_change_state(move |action, state| { 356 | let app = upgrade_weak!(weak_app); 357 | let state = state.expect("No state provided"); 358 | app.on_record_state_changed(state.into()); 359 | 360 | // Let the action store the new state 361 | action.set_state(state); 362 | }); 363 | application.add_action(&record); 364 | 365 | // When activated, reload the HTML/CSS data of the overlay 366 | let update_overlay = gio::SimpleAction::new("update_overlay", None); 367 | let weak_app = app.downgrade(); 368 | update_overlay.connect_activate(move |_action, _parameter| { 369 | let mut app = upgrade_weak!(weak_app); 370 | app.update_overlay(); 371 | }); 372 | application.add_action(&update_overlay); 373 | } 374 | 375 | // Triggers the provided action on the application 376 | pub fn trigger + IsA>(self, app: &A) { 377 | match self { 378 | Action::Quit => app.activate_action("quit", None), 379 | Action::Settings => app.activate_action("settings", None), 380 | Action::About => app.activate_action("about", None), 381 | Action::Record(new_state) => app.change_action_state("record", &new_state.into()), 382 | Action::UpdateOverlay => app.activate_action("update_overlay", None), 383 | } 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /src/audio_vumeter.rs: -------------------------------------------------------------------------------- 1 | // Ported from Voctomix's Python audio level widget: 2 | // https://github.com/voc/voctomix/blob/master/voctogui/lib/audioleveldisplay.py 3 | 4 | use cairo; 5 | use gtk::{self, prelude::*}; 6 | use num; 7 | 8 | use std::cell::RefCell; 9 | use std::ops; 10 | use std::rc::{Rc, Weak}; 11 | 12 | #[derive(Clone)] 13 | pub struct AudioVuMeter(Rc); 14 | 15 | impl ops::Deref for AudioVuMeter { 16 | type Target = AudioVuMeterInner; 17 | 18 | fn deref(&self) -> &AudioVuMeterInner { 19 | &*self.0 20 | } 21 | } 22 | 23 | #[derive(Debug)] 24 | struct LevelData { 25 | rms: Vec, 26 | peak: Vec, 27 | decay: Vec, 28 | } 29 | 30 | pub struct AudioVuMeterInner { 31 | drawing_area: gtk::DrawingArea, 32 | data: RefCell>, 33 | cached_height: RefCell>, 34 | bg_lg: RefCell>, 35 | rms_lg: RefCell>, 36 | peak_lg: RefCell>, 37 | decay_lg: RefCell>, 38 | } 39 | 40 | pub struct AudioVuMeterWeak(Weak); 41 | impl AudioVuMeterWeak { 42 | pub fn upgrade(&self) -> Option { 43 | self.0.upgrade().map(AudioVuMeter) 44 | } 45 | } 46 | 47 | impl AudioVuMeter { 48 | pub fn new() -> Self { 49 | let vumeter = AudioVuMeter(Rc::new(AudioVuMeterInner { 50 | drawing_area: gtk::DrawingArea::new(), 51 | data: RefCell::new(None), 52 | cached_height: RefCell::new(None), 53 | bg_lg: RefCell::new(None), 54 | rms_lg: RefCell::new(None), 55 | peak_lg: RefCell::new(None), 56 | decay_lg: RefCell::new(None), 57 | })); 58 | 59 | let vumeter_weak = vumeter.downgrade(); 60 | let area = vumeter.get_widget(); 61 | area.connect_draw(move |_, cr| { 62 | if let Some(mut vumeter) = vumeter_weak.upgrade() { 63 | vumeter.on_draw(cr) 64 | } else { 65 | Inhibit(false) 66 | } 67 | }); 68 | 69 | vumeter 70 | } 71 | 72 | pub fn downgrade(&self) -> AudioVuMeterWeak { 73 | AudioVuMeterWeak(Rc::downgrade(&self.0)) 74 | } 75 | 76 | pub fn get_widget(&self) -> >k::DrawingArea { 77 | &self.0.drawing_area 78 | } 79 | 80 | pub fn update(&mut self, rms: &[f64], peak: &[f64], decay: &[f64]) { 81 | *self.0.data.borrow_mut() = Some(LevelData { 82 | rms: rms.to_vec(), 83 | peak: peak.to_vec(), 84 | decay: decay.to_vec(), 85 | }); 86 | self.0.drawing_area.queue_draw(); 87 | } 88 | 89 | fn on_draw(&mut self, cr: &cairo::Context) -> Inhibit { 90 | let area = &self.0.drawing_area; 91 | let width = area.get_allocated_width(); 92 | let height = area.get_allocated_height(); 93 | 94 | let update_gradients = match *self.cached_height.borrow() { 95 | Some(h) => h != height, 96 | None => true, 97 | }; 98 | 99 | if update_gradients { 100 | *self.cached_height.borrow_mut() = Some(height); 101 | // setup gradients for all level bars 102 | *self.bg_lg.borrow_mut() = Some(self.gradient(0.25, 0.0, height.into())); 103 | *self.rms_lg.borrow_mut() = Some(self.gradient(1.0, 0.0, height.into())); 104 | *self.peak_lg.borrow_mut() = Some(self.gradient(0.75, 0.0, height.into())); 105 | *self.decay_lg.borrow_mut() = Some(self.gradient(1.0, 0.5, height.into())); 106 | } 107 | 108 | if let Some(data) = &*self.0.data.borrow() { 109 | let channels = data.rms.len() as i32; 110 | 111 | // space between the channels in px 112 | let margin = 2; 113 | 114 | // 1 channel -> 0 margins, 2 channels -> 1 margin, 3 channels… 115 | let channel_width = (width - (margin * (channels - 1))) / channels; 116 | 117 | let height_float = f64::from(height); 118 | 119 | // normalize db-value to 0…1 and multiply with the height 120 | let rms_px = data 121 | .rms 122 | .iter() 123 | .map(|db| self.normalize_db(*db) * height_float) 124 | .collect::>(); 125 | let peak_px = data 126 | .peak 127 | .iter() 128 | .map(|db| self.normalize_db(*db) * height_float) 129 | .collect::>(); 130 | let decay_px = data 131 | .decay 132 | .iter() 133 | .map(|db| self.normalize_db(*db) * height_float) 134 | .collect::>(); 135 | 136 | for channel in 0..channels { 137 | // start-coordinate for this channel 138 | let x = (channel * channel_width) + (channel * margin); 139 | let channel_idx = channel as usize; 140 | 141 | // draw background 142 | cr.rectangle( 143 | x.into(), 144 | 0.0, 145 | channel_width.into(), 146 | height_float - peak_px[channel_idx], 147 | ); 148 | 149 | if let Some(gradient) = self.bg_lg.borrow().as_ref() { 150 | cr.set_source(gradient); 151 | cr.fill(); 152 | } 153 | 154 | // draw peak bar 155 | cr.rectangle( 156 | x.into(), 157 | height_float - peak_px[channel_idx], 158 | channel_width.into(), 159 | peak_px[channel_idx], 160 | ); 161 | if let Some(gradient) = self.peak_lg.borrow().as_ref() { 162 | cr.set_source(gradient); 163 | cr.fill(); 164 | } 165 | 166 | // draw rms bar below 167 | cr.rectangle( 168 | x.into(), 169 | height_float - rms_px[channel_idx], 170 | channel_width.into(), 171 | rms_px[channel_idx] - peak_px[channel_idx], 172 | ); 173 | if let Some(gradient) = self.rms_lg.borrow().as_ref() { 174 | cr.set_source(gradient); 175 | cr.fill(); 176 | } 177 | 178 | // draw decay bar 179 | cr.rectangle( 180 | x.into(), 181 | height_float - decay_px[channel_idx], 182 | channel_width.into(), 183 | 2.0, 184 | ); 185 | if let Some(gradient) = self.decay_lg.borrow().as_ref() { 186 | cr.set_source(gradient); 187 | cr.fill(); 188 | } 189 | 190 | // draw medium grey margin bar 191 | if margin > 0 { 192 | cr.rectangle( 193 | f64::from(x) + f64::from(channel_width), 194 | 0.0, 195 | margin.into(), 196 | height.into(), 197 | ); 198 | cr.set_source_rgb(0.5, 0.5, 0.5); 199 | cr.fill(); 200 | } 201 | } 202 | 203 | for db in [-40, -20, -10, -5, -4, -3, -2, -1].iter() { 204 | let text = format!("{}", db); 205 | let extents = cr.text_extents(&text); 206 | let textwidth = extents.width; 207 | let textheight = extents.height; 208 | 209 | let y = self.normalize_db(f64::from(*db)) * height_float; 210 | if y > peak_px[channels as usize - 1] { 211 | cr.set_source_rgb(1.0, 1.0, 1.0); 212 | } else { 213 | cr.set_source_rgb(0.0, 0.0, 0.0); 214 | } 215 | 216 | cr.move_to( 217 | (f64::from(width) - textwidth) - 2.0, 218 | height_float - y - textheight, 219 | ); 220 | cr.show_text(&text); 221 | } 222 | Inhibit(true) 223 | } else { 224 | Inhibit(false) 225 | } 226 | } 227 | 228 | fn normalize_db(&self, db: f64) -> f64 { 229 | // -60db -> 1.00 (very quiet) 230 | // -30db -> 0.75 231 | // -15db -> 0.50 232 | // -5db -> 0.25 233 | // -0db -> 0.00 (very loud) 234 | let val = -0.15 * db + 1.0; 235 | let logscale = 1.0 - val.log10(); 236 | num::clamp(logscale, 0.0, 1.0) 237 | } 238 | 239 | fn gradient(&self, brightness: f64, darkness: f64, height: f64) -> cairo::LinearGradient { 240 | let lg = cairo::LinearGradient::new(0.0, 0.0, 0.0, height); 241 | lg.add_color_stop_rgb(0.0, brightness, darkness, darkness); 242 | lg.add_color_stop_rgb(0.22, brightness, brightness, darkness); 243 | lg.add_color_stop_rgb(0.25, brightness, brightness, darkness); 244 | lg.add_color_stop_rgb(0.35, darkness, brightness, darkness); 245 | lg.add_color_stop_rgb(1.0, darkness, brightness, darkness); 246 | lg 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/header_bar.rs: -------------------------------------------------------------------------------- 1 | use gio; 2 | use gtk::{self, prelude::*}; 3 | 4 | use crate::app::{Action, RecordState}; 5 | 6 | pub struct HeaderBar { 7 | record: gtk::ToggleButton, 8 | } 9 | 10 | // Create headerbar for the application 11 | // 12 | // This includes the close button and in the future will include also various buttons 13 | impl HeaderBar { 14 | pub fn new>(window: &P) -> Self { 15 | let header_bar = gtk::HeaderBar::new(); 16 | 17 | // Without this the headerbar will have no close button 18 | header_bar.set_show_close_button(true); 19 | 20 | // Create a menu button with the hamburger menu 21 | let main_menu = gtk::MenuButton::new(); 22 | let main_menu_image = 23 | gtk::Image::new_from_icon_name(Some("open-menu-symbolic"), gtk::IconSize::Menu); 24 | main_menu.set_image(Some(&main_menu_image)); 25 | 26 | // Create the menu model with the menu items. These directly activate our application 27 | // actions by their name 28 | let main_menu_model = gio::Menu::new(); 29 | main_menu_model.append(Some("Settings"), Some(Action::Settings.full_name())); 30 | main_menu_model.append(Some("About"), Some(Action::About.full_name())); 31 | main_menu.set_menu_model(Some(&main_menu_model)); 32 | 33 | // And place it on the right (end) side of the header bar 34 | header_bar.pack_end(&main_menu); 35 | 36 | // Create record button and let it trigger the record action 37 | let record_button = gtk::ToggleButton::new(); 38 | let record_button_image = 39 | gtk::Image::new_from_icon_name(Some("network-cellular"), gtk::IconSize::Menu); 40 | record_button.set_image(Some(&record_button_image)); 41 | 42 | record_button.connect_toggled(|record_button| { 43 | let app = gio::Application::get_default().expect("No default application"); 44 | Action::Record(RecordState::from(record_button.get_active())).trigger(&app); 45 | }); 46 | 47 | // Place the record button on the left 48 | header_bar.pack_start(&record_button); 49 | 50 | // Insert the headerbar as titlebar into the window 51 | window.set_titlebar(Some(&header_bar)); 52 | 53 | HeaderBar { 54 | record: record_button, 55 | } 56 | } 57 | 58 | pub fn set_record_active(&self, active: bool) { 59 | self.record.set_active(active); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | // Macro for upgrading a weak reference or returning the given value 2 | // 3 | // This works for glib/gtk objects as well as anything else providing an upgrade method 4 | macro_rules! upgrade_weak { 5 | ($x:ident, $r:expr) => {{ 6 | match $x.upgrade() { 7 | Some(o) => o, 8 | None => return $r, 9 | } 10 | }}; 11 | ($x:ident) => { 12 | upgrade_weak!($x, ()) 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | mod macros; 3 | mod about_dialog; 4 | mod app; 5 | mod audio_vumeter; 6 | mod header_bar; 7 | mod pipeline; 8 | mod settings; 9 | mod utils; 10 | 11 | use gio::prelude::*; 12 | 13 | use std::env::args; 14 | use std::error; 15 | 16 | use crate::app::App; 17 | 18 | // Unique application name to identify it 19 | // 20 | // This is used for ensuring that there's only ever a single instance of our application 21 | pub const APPLICATION_NAME: &str = "com.igalia.gstwpe.broadcast.demo"; 22 | 23 | fn main() -> Result<(), Box> { 24 | // Initialize GStreamer. This checks, among other things, what plugins are available 25 | gst::init()?; 26 | 27 | // Create an application with our name and the default flags. By default, applications can only 28 | // have a single instance and any second instance will only activate the first one again 29 | let application = 30 | gtk::Application::new(Some(APPLICATION_NAME), gio::ApplicationFlags::empty())?; 31 | 32 | // On application startup (of the first instance) we create our application. A second instance 33 | // would not run this 34 | application.connect_startup(|application| { 35 | App::on_startup(application); 36 | }); 37 | 38 | // And now run the application until the end 39 | application.run(&args().collect::>()); 40 | 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /src/pipeline.rs: -------------------------------------------------------------------------------- 1 | use base64; 2 | use glib; 3 | use gst::{self, prelude::*}; 4 | use gtk; 5 | use strfmt::strfmt; 6 | 7 | use std::cell::RefCell; 8 | use std::collections::HashMap; 9 | use std::error; 10 | use std::ops; 11 | use std::rc::{Rc, Weak}; 12 | 13 | use crate::audio_vumeter::AudioVuMeterWeak; 14 | use crate::settings::VideoResolution; 15 | use crate::utils; 16 | 17 | // Our refcounted pipeline struct for containing all the media state we have to carry around. 18 | #[derive(Clone)] 19 | pub struct Pipeline(Rc); 20 | 21 | // Deref into the contained struct to make usage a bit more ergonomic 22 | impl ops::Deref for Pipeline { 23 | type Target = PipelineInner; 24 | 25 | fn deref(&self) -> &PipelineInner { 26 | &*self.0 27 | } 28 | } 29 | 30 | pub struct PipelineInner { 31 | pipeline: gst::Pipeline, 32 | tee: gst::Element, 33 | sink: gst::Element, 34 | wpesrc: gst::Element, 35 | recording_bin: RefCell>, 36 | recording_audio_pad: RefCell>, 37 | recording_video_pad: RefCell>, 38 | audio_vumeter: AudioVuMeterWeak, 39 | } 40 | 41 | // Weak reference to our pipeline struct 42 | // 43 | // Weak references are important to prevent reference cycles. Reference cycles are cases where 44 | // struct A references directly or indirectly struct B, and struct B references struct A again 45 | // while both are using reference counting. 46 | pub struct PipelineWeak(Weak); 47 | impl PipelineWeak { 48 | pub fn upgrade(&self) -> Option { 49 | self.0.upgrade().map(Pipeline) 50 | } 51 | } 52 | 53 | fn update_overlay(wpesrc: &gst::Element, html_buffer: &str, css_buffer: &str) { 54 | const IGALIA_LOGO: &[u8] = include_bytes!("../data/igalia-logo.png"); 55 | let igalia_logo = format!("data:image/png;base64,{}", base64::encode(IGALIA_LOGO)); 56 | let igalia_logo_str = igalia_logo.as_str(); 57 | 58 | const GST_LOGO: &[u8] = include_bytes!("../data/gst-logo.svg"); 59 | let gst_logo = format!("data:image/svg+xml;base64,{}", base64::encode(GST_LOGO)); 60 | let gst_logo_str = gst_logo.as_str(); 61 | 62 | let mut vars = HashMap::new(); 63 | vars.insert("css_buffer".to_string(), &css_buffer); 64 | vars.insert("igalia_logo".to_string(), &igalia_logo_str); 65 | vars.insert("gst_logo".to_string(), &gst_logo_str); 66 | 67 | let data = &strfmt(&html_buffer, &vars).unwrap(); 68 | let bytes = glib::Bytes::from(&data.as_bytes()); 69 | wpesrc.emit("load-bytes", &[&bytes]).unwrap(); 70 | } 71 | 72 | impl Pipeline { 73 | pub fn new(audio_vumeter: AudioVuMeterWeak) -> Result> { 74 | let settings = utils::load_settings(); 75 | 76 | let (width, height) = match settings.video_resolution { 77 | VideoResolution::V480P => (640, 480), 78 | VideoResolution::V720P => (1280, 720), 79 | VideoResolution::V1080P => (1920, 1080), 80 | }; 81 | 82 | let pipeline = gst::parse_launch(&format!( 83 | "glvideomixerelement name=mixer sink_1::zorder=0 sink_1::height={height} sink_1::width={width} \ 84 | ! tee name=tee ! queue ! gtkglsink enable-last-sample=0 name=sink \ 85 | autoaudiosrc ! tee name=audio-tee ! queue ! level ! fakesink sync=1 \ 86 | wpesrc name=wpesrc draw-background=0 ! capsfilter name=wpecaps caps=\"video/x-raw(memory:GLMemory),width={width},height={height},pixel-aspect-ratio=(fraction)1/1\" ! glcolorconvert ! queue ! mixer. \ 87 | v4l2src name=videosrc ! capsfilter name=camcaps caps=\"image/jpeg,width={width},height={height},framerate=30/1\" ! decodebin ! queue ! glupload ! glcolorconvert ! queue ! mixer.", width=width, height=height) 88 | )?; 89 | 90 | // Upcast to a gst::Pipeline as the above function could've also returned an arbitrary 91 | // gst::Element if a different string was passed 92 | let pipeline = pipeline 93 | .downcast::() 94 | .expect("Couldn't downcast pipeline"); 95 | 96 | // Request that the pipeline forwards us all messages, even those that it would otherwise 97 | // aggregate first 98 | pipeline.set_property_message_forward(true); 99 | 100 | // Retrieve sink and tee elements from the pipeline for later use 101 | let tee = pipeline.get_by_name("tee").expect("No tee found"); 102 | let sink = pipeline.get_by_name("sink").expect("No sink found"); 103 | let wpesrc = pipeline.get_by_name("wpesrc").expect("No wpesrc found"); 104 | 105 | let css_buffer = include_str!("../data/style.css").to_string(); 106 | let html_buffer = include_str!("../data/index.html").to_string(); 107 | update_overlay(&wpesrc, &html_buffer, &css_buffer); 108 | 109 | let pipeline = Pipeline(Rc::new(PipelineInner { 110 | pipeline, 111 | tee, 112 | sink, 113 | wpesrc, 114 | audio_vumeter, 115 | recording_bin: RefCell::new(None), 116 | recording_audio_pad: RefCell::new(None), 117 | recording_video_pad: RefCell::new(None), 118 | })); 119 | 120 | // Install a message handler on the pipeline's bus to catch errors 121 | let bus = pipeline.pipeline.get_bus().expect("Pipeline had no bus"); 122 | 123 | // GStreamer is thread-safe and it is possible to attach bus watches from any thread, which 124 | // are then nonetheless called from the main thread. So by default, add_watch() requires 125 | // the passed closure to be Send. We want to pass non-Send values into the closure though. 126 | // 127 | // As we are on the main thread and the closure will be called on the main thread, this 128 | // is actually perfectly fine and safe to do and we can use add_watch_local(). 129 | // add_watch_local() would panic if we were not calling it from the main thread. 130 | let pipeline_weak = pipeline.downgrade(); 131 | bus.add_watch_local(move |_bus, msg| { 132 | let pipeline = upgrade_weak!(pipeline_weak, glib::Continue(false)); 133 | 134 | pipeline.on_pipeline_message(msg); 135 | 136 | glib::Continue(true) 137 | }) 138 | .expect("Unable to add bus watch"); 139 | 140 | Ok(pipeline) 141 | } 142 | 143 | pub fn refresh(&self) { 144 | let settings = utils::load_settings(); 145 | 146 | let (width, height) = match settings.video_resolution { 147 | VideoResolution::V480P => (640, 480), 148 | VideoResolution::V720P => (1280, 720), 149 | VideoResolution::V1080P => (1920, 1080), 150 | }; 151 | 152 | let cam_caps_filter = self 153 | .pipeline 154 | .get_by_name("camcaps") 155 | .expect("No webcam capsfilter found"); 156 | let mixer = self.pipeline.get_by_name("mixer").expect("No mixer found"); 157 | let wpecaps_filter = self 158 | .pipeline 159 | .get_by_name("wpecaps") 160 | .expect("No wpe capsfilter found"); 161 | 162 | cam_caps_filter.set_property_from_str( 163 | "caps", 164 | &format!( 165 | "image/jpeg,width={width},height={height},framerate=30/1", 166 | width = width, 167 | height = height 168 | ), 169 | ); 170 | wpecaps_filter.set_property_from_str("caps", &format!("video/x-raw(memory:GLMemory),width={width},height={height},pixel-aspect-ratio=(fraction)1/1", width=width, height=height)); 171 | 172 | if let Some(pad) = mixer.get_static_pad("sink_1") { 173 | pad.set_property("width", &width) 174 | .expect("No width pad property"); 175 | pad.set_property("height", &height) 176 | .expect("No height pad property"); 177 | } 178 | 179 | self.pipeline.set_state(gst::State::Paused).unwrap(); 180 | 181 | let event = gst::Event::new_reconfigure().build(); 182 | self.sink.send_event(event); 183 | 184 | self.pipeline.set_state(gst::State::Playing).unwrap(); 185 | } 186 | 187 | // Downgrade to a weak reference 188 | pub fn downgrade(&self) -> PipelineWeak { 189 | PipelineWeak(Rc::downgrade(&self.0)) 190 | } 191 | 192 | pub fn get_widget(&self) -> gtk::Widget { 193 | // Get the GTK video sink and retrieve the video display widget from it 194 | let widget_value = self 195 | .sink 196 | .get_property("widget") 197 | .expect("Sink had no widget property"); 198 | 199 | widget_value 200 | .get::() 201 | .expect("Sink's widget propery was of the wrong type") 202 | .unwrap() 203 | } 204 | 205 | pub fn start(&self) -> Result { 206 | // This has no effect if called multiple times 207 | self.pipeline.set_state(gst::State::Playing) 208 | } 209 | 210 | pub fn stop(&self) -> Result { 211 | // This has no effect if called multiple times 212 | self.pipeline.set_state(gst::State::Null) 213 | } 214 | 215 | // Start recording to the configured location 216 | pub fn start_recording(&self) -> Result<(), Box> { 217 | let settings = utils::load_settings(); 218 | 219 | if settings.rtmp_location.is_none() { 220 | return Err("Please set the RTMP end-point URL in the settings".into()); 221 | } 222 | let bin_description = &format!( 223 | "queue name=video-queue ! gldownload ! videoconvert ! {h264_encoder} ! \ 224 | flvmux streamable=1 name=mux ! rtmpsink enable-last-sample=0 location=\"{location}\" \ 225 | queue name=audio-queue ! fdkaacenc bitrate=128000 ! mux.", 226 | location = settings.rtmp_location.unwrap(), 227 | h264_encoder = settings.h264_encoder 228 | ); 229 | 230 | let bin = gst::parse_bin_from_description(bin_description, false) 231 | .map_err(|err| format!("Failed to create recording pipeline: {}", err))?; 232 | bin.set_name("recording-bin") 233 | .map_err(|err| format!("Failed to set recording bin name: {}", err))?; 234 | 235 | let video_queue = bin 236 | .get_by_name("video-queue") 237 | .expect("No video-queue found"); 238 | let audio_queue = bin 239 | .get_by_name("audio-queue") 240 | .expect("No audio-queue found"); 241 | let audio_tee = self 242 | .pipeline 243 | .get_by_name("audio-tee") 244 | .expect("No audio-tee found"); 245 | 246 | // Add the bin to the pipeline. This would only fail if there was 247 | // already a bin with the same name, which we ensured can't happen 248 | self.pipeline 249 | .add(&bin) 250 | .expect("Failed to add recording bin"); 251 | 252 | // Get our tee element by name, request a new source pad from it and then link that to our 253 | // recording bin to actually start receiving data 254 | let srcpad = self 255 | .tee 256 | .get_request_pad("src_%u") 257 | .expect("Failed to request new pad from tee"); 258 | let sinkpad = video_queue 259 | .get_static_pad("sink") 260 | .expect("Failed to get sink pad from recording bin"); 261 | 262 | *self.recording_video_pad.borrow_mut() = Some(srcpad.clone()); 263 | if let Ok(video_ghost_pad) = gst::GhostPad::new(Some("video_sink"), &sinkpad) { 264 | bin.add_pad(&video_ghost_pad).unwrap(); 265 | // If linking fails, we just undo what we did above 266 | if let Err(err) = srcpad.link(&video_ghost_pad) { 267 | // This might fail but we don't care anymore: we're in an error path 268 | let _ = self.pipeline.remove(&bin); 269 | let _ = bin.set_state(gst::State::Null); 270 | 271 | return Err( 272 | format!("Failed to link recording bin video branch: {}", err) 273 | .as_str() 274 | .into(), 275 | ); 276 | } 277 | } 278 | 279 | let audio_srcpad = audio_tee 280 | .get_request_pad("src_%u") 281 | .expect("Failed to request new pad from audio-tee"); 282 | let queue_sinkpad = audio_queue 283 | .get_static_pad("sink") 284 | .expect("Failed to get sink pad from queue"); 285 | 286 | *self.recording_audio_pad.borrow_mut() = Some(audio_srcpad.clone()); 287 | if let Ok(audio_ghost_pad) = gst::GhostPad::new(Some("audio_sink"), &queue_sinkpad) { 288 | bin.add_pad(&audio_ghost_pad).unwrap(); 289 | // If linking fails, we just undo what we did above 290 | if let Err(err) = audio_srcpad.link(&audio_ghost_pad) { 291 | // This might fail but we don't care anymore: we're in an error path 292 | let _ = self.pipeline.remove(&bin); 293 | let _ = bin.set_state(gst::State::Null); 294 | 295 | return Err( 296 | format!("Failed to link recording bin audio branch: {}", err) 297 | .as_str() 298 | .into(), 299 | ); 300 | } 301 | } 302 | 303 | bin.set_state(gst::State::Playing) 304 | .map_err(|_err| "Failed to start recording")?; 305 | 306 | *self.recording_bin.borrow_mut() = Some(bin); 307 | 308 | Ok(()) 309 | } 310 | 311 | // Stop recording if any recording was currently ongoing 312 | pub fn stop_recording(&self) { 313 | // Get our recording bin, if it does not exist then nothing has to be stopped actually. 314 | // This shouldn't really happen 315 | let bin = match self.recording_bin.borrow_mut().take() { 316 | None => return, 317 | Some(bin) => bin, 318 | }; 319 | 320 | let recordind_audio_srcpad = match self.recording_audio_pad.borrow_mut().take() { 321 | None => return, 322 | Some(bin) => bin, 323 | }; 324 | let recordind_video_srcpad = match self.recording_video_pad.borrow_mut().take() { 325 | None => return, 326 | Some(bin) => bin, 327 | }; 328 | 329 | let video_queue = bin 330 | .get_by_name("video-queue") 331 | .expect("No video-queue found"); 332 | let audio_queue = bin 333 | .get_by_name("audio-queue") 334 | .expect("No audio-queue found"); 335 | 336 | let sinkpad = video_queue 337 | .get_static_pad("sink") 338 | .expect("Failed to get video sink pad from recording bin"); 339 | 340 | // Once the tee source pad is idle and we wouldn't interfere with any data flow, unlink the 341 | // tee and the recording bin and remove/finalize the recording bin 342 | // 343 | // The closure below might be called directly from the main UI thread here or at a later 344 | // time from a GStreamer streaming thread 345 | let pipeline_weak = self.pipeline.downgrade(); 346 | recordind_video_srcpad.add_probe(gst::PadProbeType::IDLE, move |srcpad, _| { 347 | // Get the parent of the tee source pad, i.e. the tee itself 348 | if let Some(parent) = srcpad.get_parent() { 349 | if let Ok(tee) = parent.downcast::() { 350 | let _ = srcpad.unlink(&sinkpad); 351 | tee.release_request_pad(srcpad); 352 | 353 | let pipeline = upgrade_weak!(pipeline_weak, gst::PadProbeReturn::Remove); 354 | pipeline.call_async(move |pipeline| { 355 | let bin = match pipeline.get_by_name("recording-bin") { 356 | Some(bin) => bin, 357 | None => return, 358 | }; 359 | let pbin = pipeline.clone().upcast::(); 360 | // Ignore if the bin was not in the pipeline anymore for whatever 361 | // reason. It's not a problem 362 | let _ = pbin.remove(&bin); 363 | 364 | if let Err(err) = bin.set_state(gst::State::Null) { 365 | let bus = pbin.get_bus().expect("Pipeline has no bus"); 366 | let _ = bus.post(&Self::create_application_warning_message( 367 | format!("Failed to stop recording: {}", err).as_str(), 368 | )); 369 | } 370 | }); 371 | 372 | // Don't block the pad but remove the probe to let everything 373 | // continue as normal 374 | return gst::PadProbeReturn::Remove; 375 | } 376 | } 377 | gst::PadProbeReturn::Ok 378 | }); 379 | 380 | let audio_sinkpad = audio_queue 381 | .get_static_pad("sink") 382 | .expect("Failed to get audio sink pad from recording bin"); 383 | 384 | let pipeline_weak = self.pipeline.downgrade(); 385 | recordind_audio_srcpad.add_probe(gst::PadProbeType::IDLE, move |srcpad, _| { 386 | // Get the parent of the tee source pad, i.e. the tee itself 387 | if let Some(parent) = srcpad.get_parent() { 388 | if let Ok(tee) = parent.downcast::() { 389 | let _ = srcpad.unlink(&audio_sinkpad); 390 | tee.release_request_pad(srcpad); 391 | 392 | let pipeline = upgrade_weak!(pipeline_weak, gst::PadProbeReturn::Remove); 393 | pipeline.call_async(move |pipeline| { 394 | let bin = match pipeline.get_by_name("recording-bin") { 395 | Some(bin) => bin, 396 | None => return, 397 | }; 398 | 399 | let pbin = pipeline.clone().upcast::(); 400 | // Ignore if the bin was not in the pipeline anymore for whatever 401 | // reason. It's not a problem 402 | let _ = pbin.remove(&bin); 403 | 404 | if let Err(err) = bin.set_state(gst::State::Null) { 405 | let bus = pbin.get_bus().expect("Pipeline has no bus"); 406 | let _ = bus.post(&Self::create_application_warning_message( 407 | format!("Failed to stop recording: {}", err).as_str(), 408 | )); 409 | } 410 | }); 411 | 412 | // Don't block the pad but remove the probe to let everything 413 | // continue as normal 414 | return gst::PadProbeReturn::Remove; 415 | } 416 | } 417 | gst::PadProbeReturn::Ok 418 | }); 419 | } 420 | 421 | pub fn update_overlay(&self, html_buffer: &str, css_buffer: &str) { 422 | update_overlay(&self.wpesrc, html_buffer, css_buffer); 423 | } 424 | 425 | // Here we handle all message we get from the GStreamer pipeline. These are notifications sent 426 | // from GStreamer, including errors that happend at runtime. 427 | // 428 | // This is always called from the main application thread by construction. 429 | fn on_pipeline_message(&self, msg: &gst::MessageRef) { 430 | use gst::MessageView; 431 | 432 | // A message can contain various kinds of information but 433 | // here we are only interested in errors so far 434 | match msg.view() { 435 | MessageView::Error(err) => { 436 | utils::show_error_dialog( 437 | true, 438 | format!( 439 | "Error from {:?}: {} ({:?})", 440 | err.get_src().map(|s| s.get_path_string()), 441 | err.get_error(), 442 | err.get_debug() 443 | ) 444 | .as_str(), 445 | ); 446 | } 447 | MessageView::Application(msg) => match msg.get_structure() { 448 | // Here we can send ourselves messages from any thread and show them to the user in 449 | // the UI in case something goes wrong 450 | Some(s) if s.get_name() == "warning" => { 451 | let text = s 452 | .get::<&str>("text") 453 | .expect("Warning message without text") 454 | .unwrap(); 455 | utils::show_error_dialog(false, text); 456 | } 457 | _ => (), 458 | }, 459 | MessageView::Element(msg) => { 460 | if let Some(structure) = msg.get_structure() { 461 | if structure.get_name() == "level" { 462 | let rms = structure 463 | .get::("rms") 464 | .expect("level message without RMS value") 465 | .unwrap(); 466 | let rms_values = rms 467 | .iter() 468 | .map(|v| v.get_some::().unwrap()) 469 | .collect::>(); 470 | 471 | let peak = structure 472 | .get::("peak") 473 | .expect("level message without Peak value") 474 | .unwrap(); 475 | let peak_values = peak 476 | .iter() 477 | .map(|v| v.get_some::().unwrap()) 478 | .collect::>(); 479 | 480 | let decay = structure 481 | .get::("decay") 482 | .expect("level message without Decay value") 483 | .unwrap(); 484 | let decay_values = decay 485 | .iter() 486 | .map(|v| v.get_some::().unwrap()) 487 | .collect::>(); 488 | 489 | let audio_vumeter = &self.audio_vumeter; 490 | let mut vumeter = upgrade_weak!(audio_vumeter); 491 | vumeter.update(&rms_values, &peak_values, &decay_values); 492 | } 493 | } 494 | } 495 | MessageView::StateChanged(state_changed) => { 496 | if let Some(element) = msg.get_src() { 497 | if element == self.pipeline { 498 | let bin_ref = element.downcast_ref::().unwrap(); 499 | let filename = format!( 500 | "gst-wpe-broadcast-demo-{:#?}_to_{:#?}", 501 | state_changed.get_old(), 502 | state_changed.get_current() 503 | ); 504 | bin_ref.debug_to_dot_file_with_ts(gst::DebugGraphDetails::all(), filename); 505 | } 506 | } 507 | } 508 | MessageView::AsyncDone(_) => { 509 | if let Some(element) = msg.get_src() { 510 | let bin_ref = element.downcast_ref::().unwrap(); 511 | bin_ref.debug_to_dot_file_with_ts( 512 | gst::DebugGraphDetails::all(), 513 | "gst-wpe-broadcast-demo-async-done", 514 | ); 515 | } 516 | } 517 | _ => (), 518 | }; 519 | } 520 | 521 | fn create_application_warning_message(text: &str) -> gst::Message { 522 | gst::Message::new_application( 523 | gst::Structure::builder("warning") 524 | .field("text", &text) 525 | .build(), 526 | ) 527 | .build() 528 | } 529 | } 530 | -------------------------------------------------------------------------------- /src/settings.rs: -------------------------------------------------------------------------------- 1 | use gtk::{self, prelude::*}; 2 | 3 | use crate::app::App; 4 | use crate::utils; 5 | 6 | use std::cell::RefCell; 7 | use std::fs::create_dir_all; 8 | use std::ops; 9 | use std::rc::{Rc, Weak}; 10 | 11 | use serde::{Deserialize, Serialize}; 12 | 13 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Serialize, Deserialize)] 14 | pub enum VideoResolution { 15 | V480P, 16 | V720P, 17 | V1080P, 18 | } 19 | 20 | // Convenience for converting from the strings in the combobox 21 | impl From> for VideoResolution { 22 | fn from(s: Option) -> Self { 23 | if let Some(s) = s { 24 | match s.to_lowercase().as_str() { 25 | "480p" => VideoResolution::V480P, 26 | "720p" => VideoResolution::V720P, 27 | "1080p" => VideoResolution::V1080P, 28 | _ => panic!("unsupported video resolution {}", s), 29 | } 30 | } else { 31 | VideoResolution::default() 32 | } 33 | } 34 | } 35 | 36 | impl Default for VideoResolution { 37 | fn default() -> Self { 38 | VideoResolution::V720P 39 | } 40 | } 41 | 42 | #[derive(Deserialize, Serialize, Debug, Clone)] 43 | pub struct Settings { 44 | pub rtmp_location: Option, 45 | pub h264_encoder: std::string::String, 46 | pub video_resolution: VideoResolution, 47 | } 48 | 49 | impl Default for Settings { 50 | fn default() -> Settings { 51 | Settings { 52 | rtmp_location: None, 53 | h264_encoder: "video/x-raw,format=NV12 ! vaapih264enc bitrate=20000 keyframe-period=60 ! video/x-h264,profile=main".to_string(), 54 | video_resolution: VideoResolution::default(), 55 | } 56 | } 57 | } 58 | 59 | // Our refcounted settings struct for containing all the widgets we have to carry around. 60 | // 61 | // This represents our settings dialog. 62 | #[derive(Clone)] 63 | struct SettingsDialog(Rc); 64 | 65 | // Deref into the contained struct to make usage a bit more ergonomic 66 | impl ops::Deref for SettingsDialog { 67 | type Target = SettingsDialogInner; 68 | 69 | fn deref(&self) -> &SettingsDialogInner { 70 | &*self.0 71 | } 72 | } 73 | 74 | // Weak reference to our settings dialog struct 75 | // 76 | // Weak references are important to prevent reference cycles. Reference cycles are cases where 77 | // struct A references directly or indirectly struct B, and struct B references struct A again 78 | // while both are using reference counting. 79 | struct SettingsDialogWeak(Weak); 80 | 81 | impl SettingsDialogWeak { 82 | // Upgrade to a strong reference if it still exists 83 | pub fn upgrade(&self) -> Option { 84 | self.0.upgrade().map(SettingsDialog) 85 | } 86 | } 87 | 88 | struct SettingsDialogInner { 89 | rtmp_location: gtk::Entry, 90 | h264_encoder: gtk::Entry, 91 | video_resolution: gtk::ComboBoxText, 92 | } 93 | 94 | impl SettingsDialog { 95 | // Downgrade to a weak reference 96 | fn downgrade(&self) -> SettingsDialogWeak { 97 | SettingsDialogWeak(Rc::downgrade(&self.0)) 98 | } 99 | 100 | // Take current settings value from all our widgets and store into the configuration file 101 | fn save_settings(&self) { 102 | let h264_encoder = match self.h264_encoder.get_text() { 103 | Some(e) => e, 104 | None => { 105 | utils::show_error_dialog(false, "Please specify an H.264 encoder chain"); 106 | return; 107 | } 108 | }; 109 | 110 | let rtmp_location = match self.rtmp_location.get_text() { 111 | Some(l) => Some(l.into()), 112 | None => None, 113 | }; 114 | 115 | let settings = Settings { 116 | rtmp_location, 117 | h264_encoder: h264_encoder.to_string(), 118 | video_resolution: VideoResolution::from(self.video_resolution.get_active_text()), 119 | }; 120 | 121 | utils::save_settings(&settings); 122 | } 123 | } 124 | 125 | // Construct the settings dialog and ensure that the settings file exists and is loaded 126 | pub fn show_settings_dialog(application: >k::Application, app: &App) { 127 | let s = utils::get_settings_file_path(); 128 | 129 | if !s.exists() { 130 | if let Some(parent_dir) = s.parent() { 131 | if !parent_dir.exists() { 132 | if let Err(e) = create_dir_all(parent_dir) { 133 | utils::show_error_dialog( 134 | false, 135 | format!( 136 | "Error while trying to build settings snapshot_directory '{}': {}", 137 | parent_dir.display(), 138 | e 139 | ) 140 | .as_str(), 141 | ); 142 | } 143 | } 144 | } 145 | } 146 | 147 | let settings = utils::load_settings(); 148 | 149 | // Create an empty dialog with close button 150 | let dialog = gtk::Dialog::new_with_buttons( 151 | Some("WPE overlay broadcast settings"), 152 | application.get_active_window().as_ref(), 153 | gtk::DialogFlags::MODAL, 154 | &[("Close", gtk::ResponseType::Close)], 155 | ); 156 | 157 | // All the UI widgets are going to be stored in a grid 158 | let grid = gtk::Grid::new(); 159 | grid.set_column_spacing(4); 160 | grid.set_row_spacing(4); 161 | grid.set_margin_bottom(12); 162 | 163 | let resolution_label = gtk::Label::new(Some("Video resolution")); 164 | let video_resolution = gtk::ComboBoxText::new(); 165 | 166 | resolution_label.set_halign(gtk::Align::Start); 167 | 168 | video_resolution.append_text("480P"); 169 | video_resolution.append_text("720P"); 170 | video_resolution.append_text("1080P"); 171 | video_resolution.set_active(match settings.video_resolution { 172 | VideoResolution::V480P => Some(0), 173 | VideoResolution::V720P => Some(1), 174 | VideoResolution::V1080P => Some(2), 175 | }); 176 | video_resolution.set_hexpand(true); 177 | 178 | grid.attach(&resolution_label, 0, 1, 1, 1); 179 | grid.attach(&video_resolution, 1, 1, 3, 1); 180 | 181 | let rtmp_label = gtk::Label::new(Some("RTMP end-point URL")); 182 | let rtmp_location = gtk::Entry::new(); 183 | if let Some(location) = settings.rtmp_location { 184 | rtmp_location.set_text(&location); 185 | } 186 | 187 | rtmp_label.set_halign(gtk::Align::Start); 188 | 189 | grid.attach(&rtmp_label, 0, 3, 1, 1); 190 | grid.attach(&rtmp_location, 1, 3, 3, 1); 191 | 192 | let encoder_label = gtk::Label::new(Some("H.264 encoder")); 193 | let h264_encoder = gtk::Entry::new(); 194 | h264_encoder.set_text(&settings.h264_encoder); 195 | 196 | encoder_label.set_halign(gtk::Align::Start); 197 | 198 | grid.attach(&encoder_label, 0, 4, 1, 1); 199 | grid.attach(&h264_encoder, 1, 4, 3, 1); 200 | 201 | // Put the grid into the dialog's content area 202 | let content_area = dialog.get_content_area(); 203 | content_area.pack_start(&grid, true, true, 0); 204 | content_area.set_border_width(10); 205 | 206 | let settings_dialog = SettingsDialog(Rc::new(SettingsDialogInner { 207 | rtmp_location, 208 | h264_encoder, 209 | video_resolution, 210 | })); 211 | 212 | let settings_dialog_weak = settings_dialog.downgrade(); 213 | settings_dialog 214 | .rtmp_location 215 | .connect_property_text_notify(move |_| { 216 | let settings_dialog = upgrade_weak!(settings_dialog_weak); 217 | settings_dialog.save_settings(); 218 | }); 219 | 220 | let settings_dialog_weak = settings_dialog.downgrade(); 221 | settings_dialog 222 | .h264_encoder 223 | .connect_property_text_notify(move |_| { 224 | let settings_dialog = upgrade_weak!(settings_dialog_weak); 225 | settings_dialog.save_settings(); 226 | }); 227 | 228 | let settings_dialog_weak = settings_dialog.downgrade(); 229 | let weak_app = app.downgrade(); 230 | settings_dialog.video_resolution.connect_changed(move |_| { 231 | let settings_dialog = upgrade_weak!(settings_dialog_weak); 232 | settings_dialog.save_settings(); 233 | let app = upgrade_weak!(weak_app); 234 | app.refresh_pipeline(); 235 | }); 236 | 237 | // Close the dialog when the close button is clicked. We don't need to save the settings here 238 | // as we already did that whenever the user changed something in the UI. 239 | // 240 | // The closure keeps the one and only strong reference to our settings dialog struct and it 241 | // will be freed once the dialog is destroyed 242 | let settings_dialog_storage = RefCell::new(Some(settings_dialog)); 243 | let weak_app = app.downgrade(); 244 | dialog.connect_response(move |dialog, _| { 245 | dialog.destroy(); 246 | 247 | let _ = settings_dialog_storage.borrow_mut().take(); 248 | let app = upgrade_weak!(weak_app); 249 | app.refresh_pipeline(); 250 | }); 251 | 252 | dialog.set_resizable(false); 253 | dialog.show_all(); 254 | } 255 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use gio::{self, prelude::*}; 2 | use glib; 3 | use gtk::{self, prelude::*}; 4 | 5 | use std::path::PathBuf; 6 | 7 | use serde_any; 8 | 9 | use crate::settings::Settings; 10 | use crate::APPLICATION_NAME; 11 | 12 | // Get the default path for the settings file 13 | pub fn get_settings_file_path() -> PathBuf { 14 | let mut path = glib::get_user_config_dir().unwrap_or_else(|| PathBuf::from(".")); 15 | path.push(APPLICATION_NAME); 16 | path.push("settings.toml"); 17 | path 18 | } 19 | 20 | // Save the provided settings to the settings path 21 | pub fn save_settings(settings: &Settings) { 22 | let s = get_settings_file_path(); 23 | if let Err(e) = serde_any::to_file(&s, &settings) { 24 | show_error_dialog( 25 | false, 26 | format!("Error while trying to save file: {}", e).as_str(), 27 | ); 28 | } 29 | } 30 | 31 | // Load the current settings 32 | pub fn load_settings() -> Settings { 33 | let s = get_settings_file_path(); 34 | if s.exists() && s.is_file() { 35 | match serde_any::from_file::(&s) { 36 | Ok(s) => s, 37 | Err(e) => { 38 | show_error_dialog( 39 | false, 40 | format!("Error while opening '{}': {}", s.display(), e).as_str(), 41 | ); 42 | Settings::default() 43 | } 44 | } 45 | } else { 46 | Settings::default() 47 | } 48 | } 49 | 50 | // Shows an error dialog, and if it's fatal it will quit the application once 51 | // the dialog is closed 52 | pub fn show_error_dialog(fatal: bool, text: &str) { 53 | let app = gio::Application::get_default() 54 | .expect("No default application") 55 | .downcast::() 56 | .expect("Default application has wrong type"); 57 | 58 | let dialog = gtk::MessageDialog::new( 59 | app.get_active_window().as_ref(), 60 | gtk::DialogFlags::MODAL, 61 | gtk::MessageType::Error, 62 | gtk::ButtonsType::Ok, 63 | text, 64 | ); 65 | 66 | dialog.connect_response(move |dialog, _| { 67 | let app = gio::Application::get_default().expect("No default application"); 68 | 69 | dialog.destroy(); 70 | 71 | if fatal { 72 | app.quit(); 73 | } 74 | }); 75 | 76 | dialog.set_resizable(false); 77 | dialog.show_all(); 78 | } 79 | --------------------------------------------------------------------------------