gst-plugin-fallbackswitch-0.14.3/.cargo_vcs_info.json0000644000000001620000000000100161700ustar { "git": { "sha1": "f5ba8124f1b86484a399599c810d95a9419b12be" }, "path_in_vcs": "utils/fallbackswitch" }gst-plugin-fallbackswitch-0.14.3/Cargo.lock0000644000001111570000000000100141520ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "async-channel" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "atomic_refcell" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "cairo-rs" version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfe4354df4da648870e363387679081f8f9fc538ec8b55901e3740c6a0ef81b1" dependencies = [ "bitflags", "cairo-sys-rs", "glib", "libc", ] [[package]] name = "cairo-sys-rs" version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47d6c3300c7103eb8e4de07591003511aa25664438f8c6fc317a3a9902c103f8" dependencies = [ "glib-sys", "libc", "system-deps", ] [[package]] name = "cc" version = "1.2.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" dependencies = [ "find-msvc-tools", "shlex", ] [[package]] name = "cfg-expr" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a2b34126159980f92da2a08bdec0694fd80fb5eb9e48aff25d20a0d8dfa710d" dependencies = [ "smallvec", "target-lexicon", ] [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", "num-traits", "windows-link", ] [[package]] name = "concurrent-queue" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "event-listener" version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener-strategy" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ "event-listener", "pin-project-lite", ] [[package]] name = "field-offset" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" dependencies = [ "memoffset", "rustc_version", ] [[package]] name = "find-msvc-tools" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" [[package]] name = "futures-channel" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", ] [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-sink" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-macro", "futures-task", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "gdk-pixbuf" version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a3c64459f569154f37616fc28923bfac490d4aaa134aaf5eca58a2c0c13050f" dependencies = [ "gdk-pixbuf-sys", "gio", "glib", "libc", ] [[package]] name = "gdk-pixbuf-sys" version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3854ef7a6a8b8f3b4013a01d5f9cb0d1794ec4e810c6cb4e2cc6d980f1baf724" dependencies = [ "gio-sys", "glib-sys", "gobject-sys", "libc", "system-deps", ] [[package]] name = "gdk4" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7e292649dc26e3440c508a00f42ab39156008320dd6e962d63eaf626ba4d7f0" dependencies = [ "cairo-rs", "gdk-pixbuf", "gdk4-sys", "gio", "glib", "libc", "pango", ] [[package]] name = "gdk4-sys" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4f3174fa4f1e0bf2a7e04469b65db8f4d1db89a6f5cdc57727b14e97ce438cf" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", "gio-sys", "glib-sys", "gobject-sys", "libc", "pango-sys", "pkg-config", "system-deps", ] [[package]] name = "gdk4-win32" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09f88ca409b0e2c814f4a30e78a0ba5f8b5133e2c95ad4f0b57c9a98d1337620" dependencies = [ "gdk4", "gdk4-win32-sys", "gio", "glib", "libc", ] [[package]] name = "gdk4-win32-sys" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8802202ddc9a4b682ed5cb8e2f3bf9eece0ee55edc938c775ae02378839938" dependencies = [ "gdk4-sys", "glib-sys", "libc", "system-deps", ] [[package]] name = "getrandom" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", "wasip2", ] [[package]] name = "gio" version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daeff3dd716d1ba91850b976b76a1c2d28f99ef6c1602cd8fdaa8fab8017fd9c" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-util", "gio-sys", "glib", "libc", "pin-project-lite", "smallvec", ] [[package]] name = "gio-sys" version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "171ed2f6dd927abbe108cfd9eebff2052c335013f5879d55bab0dc1dee19b706" dependencies = [ "glib-sys", "gobject-sys", "libc", "system-deps", "windows-sys", ] [[package]] name = "glib" version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b9dbecb1c33e483a98be4acfea2ab369e1c28f517c6eadb674537409c25c4b2" dependencies = [ "bitflags", "futures-channel", "futures-core", "futures-executor", "futures-task", "futures-util", "gio-sys", "glib-macros", "glib-sys", "gobject-sys", "libc", "memchr", "smallvec", ] [[package]] name = "glib-macros" version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "880e524e0085f3546cfb38532b2c202c0d64741d9977a6e4aa24704bfc9f19fb" dependencies = [ "heck", "proc-macro-crate", "proc-macro2", "quote", "syn", ] [[package]] name = "glib-sys" version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d09d3d0fddf7239521674e57b0465dfbd844632fec54f059f7f56112e3f927e1" dependencies = [ "libc", "system-deps", ] [[package]] name = "gobject-sys" version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "538e41d8776173ec107e7b0f2aceced60abc368d7e1d81c1f0e2ecd35f59080d" dependencies = [ "glib-sys", "libc", "system-deps", ] [[package]] name = "graphene-rs" version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7749aaf5d3b955bf3bfce39e3423705878a666b561384134da0e7786a45ddc3" dependencies = [ "glib", "graphene-sys", "libc", ] [[package]] name = "graphene-sys" version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250abaee850a90a276509890a78029c356173f9573412bded5f155b0e41fa568" dependencies = [ "glib-sys", "libc", "pkg-config", "system-deps", ] [[package]] name = "gsk4" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6687e9f92ca89c000c376400cfaf7914d099413d72fdf4f84a25775a0b1fb2d" dependencies = [ "cairo-rs", "gdk4", "glib", "graphene-rs", "gsk4-sys", "libc", "pango", ] [[package]] name = "gsk4-sys" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e76bcf64d9c4846f19651f45b400cc0c9c4c17b651849da520f3d77c6988c52" dependencies = [ "cairo-sys-rs", "gdk4-sys", "glib-sys", "gobject-sys", "graphene-sys", "libc", "pango-sys", "system-deps", ] [[package]] name = "gst-plugin-fallbackswitch" version = "0.14.3" dependencies = [ "gio", "gst-plugin-gtk4", "gst-plugin-version-helper", "gstreamer", "gstreamer-app", "gstreamer-audio", "gstreamer-base", "gstreamer-check", "gstreamer-video", "gtk4", "parking_lot", "rand", ] [[package]] name = "gst-plugin-gtk4" version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc3ac5bcd3b6d159d4f4bf521efee0db52c97ea1a731f1721ba8a52828b331fb" dependencies = [ "async-channel", "gdk4-win32", "gst-plugin-version-helper", "gstreamer", "gstreamer-base", "gstreamer-gl", "gstreamer-video", "gtk4", "windows-sys", ] [[package]] name = "gst-plugin-version-helper" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a68a894ef2d738054b950e1dbef5d9012b63fd968d4d32dbccd31bd8d8d4b219" dependencies = [ "chrono", "toml_edit", ] [[package]] name = "gstreamer" version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69ac2f12970a2f85a681d2ceaa40c32fe86cc202ead315e0dfa2223a1217cd24" dependencies = [ "cfg-if", "futures-channel", "futures-core", "futures-util", "glib", "gstreamer-sys", "itertools", "kstring", "libc", "muldiv", "num-integer", "num-rational", "option-operations", "pastey", "pin-project-lite", "smallvec", "thiserror", ] [[package]] name = "gstreamer-app" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0af5d403738faf03494dfd502d223444b4b44feb997ba28ab3f118ee6d40a0b2" dependencies = [ "futures-core", "futures-sink", "glib", "gstreamer", "gstreamer-app-sys", "gstreamer-base", "libc", ] [[package]] name = "gstreamer-app-sys" version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aaf1a3af017f9493c34ccc8439cbce5c48f6ddff6ec0514c23996b374ff25f9a" dependencies = [ "glib-sys", "gstreamer-base-sys", "gstreamer-sys", "libc", "system-deps", ] [[package]] name = "gstreamer-audio" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68e540174d060cd0d7ee2c2356f152f05d8262bf102b40a5869ff799377269d8" dependencies = [ "cfg-if", "glib", "gstreamer", "gstreamer-audio-sys", "gstreamer-base", "libc", "smallvec", ] [[package]] name = "gstreamer-audio-sys" version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "626cd3130bc155a8b6d4ac48cfddc15774b5a6cc76fcb191aab09a2655bad8f5" dependencies = [ "glib-sys", "gobject-sys", "gstreamer-base-sys", "gstreamer-sys", "libc", "system-deps", ] [[package]] name = "gstreamer-base" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71ff9b0bbc8041f0c6c8a53b206a6542f86c7d9fa8a7dff3f27d9c374d9f39b4" dependencies = [ "atomic_refcell", "cfg-if", "glib", "gstreamer", "gstreamer-base-sys", "libc", ] [[package]] name = "gstreamer-base-sys" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed78852b92db1459b8f4288f86e6530274073c20be2f94ba642cddaca08b00e" dependencies = [ "glib-sys", "gobject-sys", "gstreamer-sys", "libc", "system-deps", ] [[package]] name = "gstreamer-check" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb2ebc727c1ef99e83e994266991facaca87b79e302f146f9a2834bfc3455a94" dependencies = [ "glib", "gstreamer", "gstreamer-check-sys", ] [[package]] name = "gstreamer-check-sys" version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac87f71afbc3fadcdc4a73baf6bcc7b4b8e80e740798d13d1f783d2dd4a6b225" dependencies = [ "glib-sys", "gobject-sys", "gstreamer-sys", "libc", "system-deps", ] [[package]] name = "gstreamer-gl" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1a96afcc53a219607e797af298c50537a0cae0f54414c860209a21b0b660c3b" dependencies = [ "glib", "gstreamer", "gstreamer-base", "gstreamer-gl-sys", "gstreamer-video", "libc", ] [[package]] name = "gstreamer-gl-sys" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbad7be8c51998c1a4645b0e165b424442649081109aaf30d503d38a57e6f23c" dependencies = [ "glib-sys", "gobject-sys", "gstreamer-base-sys", "gstreamer-sys", "gstreamer-video-sys", "libc", "system-deps", ] [[package]] name = "gstreamer-sys" version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a24ae2930e683665832a19ef02466094b09d1f2da5673f001515ed5486aa9377" dependencies = [ "cfg-if", "glib-sys", "gobject-sys", "libc", "system-deps", ] [[package]] name = "gstreamer-video" version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94ab2e323a4ed2d9cbe65953eafe5dbffa5569120dd4e23ecf36727a40e64a97" dependencies = [ "cfg-if", "futures-channel", "glib", "gstreamer", "gstreamer-base", "gstreamer-video-sys", "libc", "thiserror", ] [[package]] name = "gstreamer-video-sys" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d944b1492bdd7a72a02ae9a5da6e34a29194b8623d3bd02752590b06fb837a7" dependencies = [ "glib-sys", "gobject-sys", "gstreamer-base-sys", "gstreamer-sys", "libc", "system-deps", ] [[package]] name = "gtk4" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58ea71795b91a0725b0e926e72e3d209d920ce60166e3a8f9f4dd46f287fee87" dependencies = [ "cairo-rs", "field-offset", "futures-channel", "gdk-pixbuf", "gdk4", "gio", "glib", "graphene-rs", "gsk4", "gtk4-macros", "gtk4-sys", "libc", "pango", ] [[package]] name = "gtk4-macros" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "821160b4f17e7e4ed748818c23682d0a46bed04c287dbaac54dd4869d2c5e06a" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", ] [[package]] name = "gtk4-sys" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d274cbaf7d9aa55b7aff78cb21b43299d64e514e1300671469b66f691cc5a011" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", "gdk4-sys", "gio-sys", "glib-sys", "gobject-sys", "graphene-sys", "gsk4-sys", "libc", "pango-sys", "system-deps", ] [[package]] name = "hashbrown" version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "iana-time-zone" version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "log", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "indexmap" version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "itertools" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] [[package]] name = "js-sys" version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "kstring" version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" dependencies = [ "static_assertions", ] [[package]] name = "libc" version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "lock_api" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ "scopeguard", ] [[package]] name = "log" version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memoffset" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] [[package]] name = "muldiv" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" [[package]] name = "num-integer" version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ "num-traits", ] [[package]] name = "num-rational" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ "num-integer", "num-traits", ] [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "option-operations" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b31ce827892359f23d3cd1cc4c75a6c241772bbd2db17a92dcf27cbefdf52689" dependencies = [ "pastey", ] [[package]] name = "pango" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e37b7a678e18c2e9f2485f7e39b7b2dac99590d5ddef08a7f56eae38a145402e" dependencies = [ "gio", "glib", "libc", "pango-sys", ] [[package]] name = "pango-sys" version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4f5daf21da43fba9f2a0092da0eebeb77637c23552bccaf58f791c518009c94" dependencies = [ "glib-sys", "gobject-sys", "libc", "system-deps", ] [[package]] name = "parking" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-link", ] [[package]] name = "pastey" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro-crate" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom", ] [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_spanned" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" dependencies = [ "serde_core", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "slab" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "syn" version = "2.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "system-deps" version = "7.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c236d79f20808ca0084bfcd1a2fd6c686216b7f7a0c4fc39deb0cbf5eaab3713" dependencies = [ "cfg-expr", "heck", "pkg-config", "toml", "version-compare", ] [[package]] name = "target-lexicon" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "toml" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" dependencies = [ "indexmap", "serde_core", "serde_spanned", "toml_datetime", "toml_parser", "toml_writer", "winnow", ] [[package]] name = "toml_datetime" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ "indexmap", "toml_datetime", "toml_parser", "winnow", ] [[package]] name = "toml_parser" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ "winnow", ] [[package]] name = "toml_writer" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "version-compare" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ "bumpalo", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] [[package]] name = "windows-core" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-implement" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-interface" version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "zerocopy" version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", "syn", ] gst-plugin-fallbackswitch-0.14.3/Cargo.toml0000644000000050070000000000100141710ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.83" name = "gst-plugin-fallbackswitch" version = "0.14.3" authors = [ "Sebastian Dröge ", "Jan Schmidt ", ] build = "build.rs" autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "GStreamer Fallback Switcher and Source Plugin" readme = false license = "MPL-2.0" repository = "https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs" [package.metadata.capi] min_version = "0.9.21" [package.metadata.capi.header] enabled = false [package.metadata.capi.library] install_subdir = "gstreamer-1.0" versioning = false import_library = false [package.metadata.capi.pkg_config] requires_private = "gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0" [package.metadata.gstreamer] release_date = "2025-10-31" [features] capi = [] default = ["v1_20"] doc = ["gst/v1_18"] static = [] v1_20 = ["gst/v1_20"] [lib] name = "gstfallbackswitch" crate-type = [ "cdylib", "rlib", ] path = "src/lib.rs" [[example]] name = "gtk-fallbackswitch" path = "examples/gtk_fallbackswitch.rs" required-features = [ "gtk", "gio", "gst-plugin-gtk4", ] [[test]] name = "fallbackswitch" path = "tests/fallbackswitch.rs" [dependencies.gio] version = "0.21" optional = true [dependencies.gst] version = "0.24" package = "gstreamer" [dependencies.gst-audio] version = "0.24" package = "gstreamer-audio" [dependencies.gst-base] version = "0.24" package = "gstreamer-base" [dependencies.gst-plugin-gtk4] version = "0.14" optional = true [dependencies.gst-video] version = "0.24" package = "gstreamer-video" [dependencies.gtk] version = "0.10" features = ["v4_6"] optional = true package = "gtk4" [dependencies.parking_lot] version = "0.12" [dependencies.rand] version = "0.9" [dev-dependencies.gst-app] version = "0.24" package = "gstreamer-app" [dev-dependencies.gst-check] version = "0.24" package = "gstreamer-check" [build-dependencies.gst-plugin-version-helper] version = "0.8" gst-plugin-fallbackswitch-0.14.3/Cargo.toml.orig000064400000000000000000000030031046102023000176440ustar 00000000000000[package] name = "gst-plugin-fallbackswitch" version.workspace = true authors = ["Sebastian Dröge ", "Jan Schmidt "] repository.workspace = true license = "MPL-2.0" edition.workspace = true rust-version.workspace = true description = "GStreamer Fallback Switcher and Source Plugin" [dependencies] gst.workspace = true gst-base.workspace = true gst-audio.workspace = true gst-video.workspace = true gst-plugin-gtk4 = { path = "../../video/gtk4", optional = true, version = "0.14" } gtk = { workspace = true, optional = true } gio = { workspace = true, optional = true } parking_lot = "0.12" rand = "0.9" [dev-dependencies] gst-app.workspace = true gst-check.workspace = true [lib] name = "gstfallbackswitch" crate-type = ["cdylib", "rlib"] path = "src/lib.rs" [[example]] name = "gtk-fallbackswitch" path = "examples/gtk_fallbackswitch.rs" required-features = ["gtk", "gio", "gst-plugin-gtk4"] [build-dependencies] gst-plugin-version-helper.workspace = true [features] default = ["v1_20"] static = [] capi = [] doc = ["gst/v1_18"] v1_20 = ["gst/v1_20"] [package.metadata.capi] min_version = "0.9.21" [package.metadata.capi.header] enabled = false [package.metadata.capi.library] install_subdir = "gstreamer-1.0" versioning = false import_library = false [package.metadata.capi.pkg_config] requires_private = "gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0" [package.metadata.gstreamer] release_date = "2025-10-31" gst-plugin-fallbackswitch-0.14.3/LICENSE-MPL-2.0000064400000000000000000000405261046102023000167200ustar 00000000000000Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. gst-plugin-fallbackswitch-0.14.3/build.rs000064400000000000000000000000651046102023000164270ustar 00000000000000fn main() { gst_plugin_version_helper::info(); } gst-plugin-fallbackswitch-0.14.3/examples/gtk_fallbackswitch.rs000064400000000000000000000147531046102023000230050ustar 00000000000000// Copyright (C) 2019 Sebastian Dröge // // This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. // If a copy of the MPL was not distributed with this file, You can obtain one at // . // // SPDX-License-Identifier: MPL-2.0 use gst::glib; use gst::prelude::*; use gtk::prelude::*; use std::cell::RefCell; const MAIN_PIPELINE: &str = "videotestsrc is-live=true pattern=ball"; const FALLBACK_PIPELINE: &str = "videotestsrc is-live=true pattern=snow"; //const MAIN_PIPELINE: &str = "videotestsrc is-live=true pattern=ball ! x264enc tune=zerolatency"; //const FALLBACK_PIPELINE: &str = "videotestsrc is-live=true pattern=snow ! x264enc tune=zerolatency"; fn create_pipeline() -> (gst::Pipeline, gst::Pad, gst::Element) { let pipeline = gst::Pipeline::default(); let video_src = gst::parse::bin_from_description(MAIN_PIPELINE, true) .unwrap() .upcast(); let fallback_video_src = gst::parse::bin_from_description(FALLBACK_PIPELINE, true) .unwrap() .upcast(); let fallbackswitch = gst::ElementFactory::make("fallbackswitch") .property("timeout", gst::ClockTime::SECOND) .build() .unwrap(); let decodebin = gst::ElementFactory::make("decodebin").build().unwrap(); let videoconvert = gst::ElementFactory::make("videoconvert").build().unwrap(); let videoconvert_clone = videoconvert.clone(); decodebin.connect_pad_added(move |_, pad| { let caps = pad.current_caps().unwrap(); let s = caps.structure(0).unwrap(); let sinkpad = videoconvert_clone.static_pad("sink").unwrap(); if s.name() == "video/x-raw" && !sinkpad.is_linked() { pad.link(&sinkpad).unwrap(); } }); let video_sink = gst::ElementFactory::make("gtk4paintablesink") .build() .unwrap(); pipeline .add_many([ &video_src, &fallback_video_src, &fallbackswitch, &decodebin, &videoconvert, &video_sink, ]) .unwrap(); /* The first pad requested will be automatically preferred */ video_src .link_pads(Some("src"), &fallbackswitch, Some("sink_%u")) .unwrap(); fallback_video_src .link_pads(Some("src"), &fallbackswitch, Some("sink_%u")) .unwrap(); fallbackswitch .link_pads(Some("src"), &decodebin, Some("sink")) .unwrap(); videoconvert .link_pads(Some("src"), &video_sink, Some("sink")) .unwrap(); (pipeline, video_src.static_pad("src").unwrap(), video_sink) } fn create_ui(app: >k::Application) { let (pipeline, video_src_pad, video_sink) = create_pipeline(); let window = gtk::ApplicationWindow::new(app); window.set_default_size(320, 240); let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); let picture = gtk::Picture::new(); let paintable = video_sink.property::("paintable"); picture.set_paintable(Some(&paintable)); vbox.append(&picture); let position_label = gtk::Label::new(Some("Position: 00:00:00")); vbox.append(&position_label); let drop_button = gtk::ToggleButton::with_label("Drop Signal"); vbox.append(&drop_button); window.set_child(Some(&vbox)); window.present(); app.add_window(&window); let video_sink_weak = video_sink.downgrade(); let timeout_id = glib::timeout_add_local(std::time::Duration::from_millis(100), move || { let Some(video_sink) = video_sink_weak.upgrade() else { return glib::ControlFlow::Break; }; let position = video_sink .query_position::() .unwrap_or(gst::ClockTime::ZERO); position_label.set_text(&format!("Position: {position:.1}")); glib::ControlFlow::Continue }); let video_src_pad_weak = video_src_pad.downgrade(); let drop_id = RefCell::new(None); drop_button.connect_toggled(move |drop_button| { let Some(video_src_pad) = video_src_pad_weak.upgrade() else { return; }; let drop = drop_button.is_active(); if drop { let mut drop_id = drop_id.borrow_mut(); if drop_id.is_none() { *drop_id = video_src_pad .add_probe(gst::PadProbeType::BUFFER, |_, _| gst::PadProbeReturn::Drop); } } else if let Some(drop_id) = drop_id.borrow_mut().take() { video_src_pad.remove_probe(drop_id); } }); let app_weak = app.downgrade(); window.connect_close_request(move |_| { let Some(app) = app_weak.upgrade() else { return glib::Propagation::Stop; }; app.quit(); glib::Propagation::Stop }); let bus = pipeline.bus().unwrap(); let app_weak = app.downgrade(); let bus_watch = bus .add_watch_local(move |_, msg| { use gst::MessageView; let Some(app) = app_weak.upgrade() else { return glib::ControlFlow::Break; }; match msg.view() { MessageView::Eos(..) => app.quit(), MessageView::Error(err) => { println!( "Error from {:?}: {} ({:?})", msg.src().map(|s| s.path_string()), err.error(), err.debug() ); app.quit(); } _ => (), }; glib::ControlFlow::Continue }) .expect("Failed to add bus watch"); pipeline.set_state(gst::State::Playing).unwrap(); // Pipeline reference is owned by the closure below, so will be // destroyed once the app is destroyed let timeout_id = RefCell::new(Some(timeout_id)); let bus_watch = RefCell::new(Some(bus_watch)); app.connect_shutdown(move |_| { drop(bus_watch.borrow_mut().take()); pipeline.set_state(gst::State::Null).unwrap(); if let Some(timeout_id) = timeout_id.borrow_mut().take() { timeout_id.remove(); } }); } fn main() -> glib::ExitCode { gst::init().unwrap(); gtk::init().unwrap(); gstfallbackswitch::plugin_register_static().expect("Failed to register fallbackswitch plugin"); gstgtk4::plugin_register_static().expect("Failed to register gtk4paintablesink plugin"); let app = gtk::Application::new(None::<&str>, gio::ApplicationFlags::FLAGS_NONE); app.connect_activate(create_ui); app.run() } gst-plugin-fallbackswitch-0.14.3/src/fallbacksrc/custom_source/imp.rs000064400000000000000000000542511046102023000240530ustar 00000000000000// Copyright (C) 2020 Sebastian Dröge // // This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. // If a copy of the MPL was not distributed with this file, You can obtain one at // . // // SPDX-License-Identifier: MPL-2.0 use gst::glib::SignalHandlerId; use gst::glib::{self, GString}; use gst::prelude::*; use gst::subclass::prelude::*; use std::sync::MutexGuard; use std::{ mem, sync::{Mutex, OnceLock}, }; use std::sync::LazyLock; static CAT: LazyLock = LazyLock::new(|| { gst::DebugCategory::new( "fallbacksrc-custom-source", gst::DebugColorFlags::empty(), Some("Fallback Custom Source Bin"), ) }); struct Stream { source_pad: gst::Pad, ghost_pad: gst::GhostPad, stream: gst::Stream, // Used if source isn't stream-aware and we're handling the stream selection manually is_selected: bool, } impl Stream { // If source isn't stream-aware, we expose pads only after no-more-pads and READY->PAUSED fn is_exposed(&self) -> bool { self.ghost_pad.parent().is_some() } } #[derive(Default)] struct State { stream_id_prefix: String, received_collection: Option, pads: Vec, num_audio: usize, num_video: usize, pad_added_sig_id: Option, pad_removed_sig_id: Option, no_more_pads_sig_id: Option, selection_seqnum: Option, // Signals either: // - ready->paused to post collection after no-more-pads was already called // - or no-more-pads to post collection because we're after ready->paused should_post_collection: bool, } impl State { /// If source sent us a collection, it's stream-aware /// and we can just forward the collection and selection events fn is_passthrough(&self) -> bool { self.received_collection.is_some() } fn our_collection(&self) -> gst::StreamCollection { let streams = self.pads.iter().map(|p| p.stream.clone()); gst::StreamCollection::builder(None) .streams(streams) .build() } } #[derive(Default)] pub struct CustomSource { source: OnceLock, state: Mutex, } #[glib::object_subclass] impl ObjectSubclass for CustomSource { const NAME: &'static str = "GstFallbackSrcCustomSource"; type Type = super::CustomSource; type ParentType = gst::Bin; } impl ObjectImpl for CustomSource { fn properties() -> &'static [glib::ParamSpec] { static PROPERTIES: LazyLock> = LazyLock::new(|| { vec![glib::ParamSpecObject::builder::("source") .nick("Source") .blurb("Source") .write_only() .construct_only() .build()] }); PROPERTIES.as_ref() } fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { match pspec.name() { "source" => { let source = value.get::().unwrap(); self.source.set(source.clone()).unwrap(); self.obj().add(&source).unwrap(); } _ => unreachable!(), } } fn constructed(&self) { self.parent_constructed(); let obj = self.obj(); obj.set_suppressed_flags(gst::ElementFlags::SOURCE | gst::ElementFlags::SINK); obj.set_element_flags(gst::ElementFlags::SOURCE); obj.set_bin_flags(gst::BinFlags::STREAMS_AWARE); } } impl GstObjectImpl for CustomSource {} impl ElementImpl for CustomSource { fn pad_templates() -> &'static [gst::PadTemplate] { static PAD_TEMPLATES: LazyLock> = LazyLock::new(|| { let audio_src_pad_template = gst::PadTemplate::new( "audio_%u", gst::PadDirection::Src, gst::PadPresence::Sometimes, &gst::Caps::new_any(), ) .unwrap(); let video_src_pad_template = gst::PadTemplate::new( "video_%u", gst::PadDirection::Src, gst::PadPresence::Sometimes, &gst::Caps::new_any(), ) .unwrap(); vec![audio_src_pad_template, video_src_pad_template] }); PAD_TEMPLATES.as_ref() } #[allow(clippy::single_match)] fn change_state( &self, transition: gst::StateChange, ) -> Result { match transition { gst::StateChange::NullToReady => { self.start()?; } gst::StateChange::ReadyToPaused => { let mut state = self.state.lock().unwrap(); if !state.is_passthrough() { if state.should_post_collection { self.post_collection(state); } else { // Tells no-more-pads handler it can post the collection right away state.should_post_collection = true; } } } _ => (), } let res = self.parent_change_state(transition)?; match transition { gst::StateChange::ReadyToNull | gst::StateChange::NullToNull => { self.stop(); } _ => (), } Ok(res) } fn send_event(&self, event: gst::Event) -> bool { match event.view() { gst::EventView::SelectStreams(e) => { if self.state.lock().unwrap().is_passthrough() { gst::debug!(CAT, imp = self, "Forwarding select streams event to source"); let streams = e.streams(); let event = gst::event::SelectStreams::builder(streams.iter().map(|s| s.as_str())) .build(); return self.source.get().unwrap().send_event(event); } gst::debug!(CAT, imp = self, "Handling select streams event"); let stream_ids = e .streams() .into_iter() .map(glib::GString::from) .collect::>(); if let Some(message) = self.handle_stream_selection(stream_ids) { let mut state = self.state.lock().unwrap(); state.selection_seqnum = Some(e.seqnum()); drop(state); if let Err(err) = self.obj().post_message(message) { gst::warning!(CAT, imp = self, "Failed to post message: {}", err); } return true; } false } _ => true, } } } impl BinImpl for CustomSource { #[allow(clippy::single_match)] fn handle_message(&self, msg: gst::Message) { use gst::MessageView; match msg.view() { MessageView::StreamCollection(collection) => { // Receiving a stream collection indicates we can be in passthrough mode // Otherwise if no collection is received, we generate our own one and handle selection etc. gst::debug!( CAT, imp = self, "Forwarding stream collection message from source: {:?}", collection.stream_collection() ); let mut state = self.state.lock().unwrap(); state.received_collection = Some(collection.stream_collection().clone()); drop(state); let message = gst::message::StreamCollection::builder(&collection.stream_collection()) .src(&*self.obj()) .build(); if let Err(err) = self.obj().post_message(message) { gst::warning!(CAT, imp = self, "Failed to post message: {}", err); } } MessageView::StreamsSelected(selected) => { gst::debug!( CAT, imp = self, "Forwarding streams-selected from source: {:?}", selected.streams() ); let message = gst::message::StreamsSelected::builder(&selected.stream_collection()) .streams(selected.streams()) .src(&*self.obj()) .build(); if let Err(err) = self.obj().post_message(message) { gst::warning!(CAT, imp = self, "Failed to post message: {}", err); } } _ => self.parent_handle_message(msg), } } } impl CustomSource { fn start(&self) -> Result { gst::debug!(CAT, imp = self, "Starting"); let source = self.source.get().unwrap(); let mut state = self.state.lock().unwrap(); state.stream_id_prefix = format!("{:016x}", rand::random::()); drop(state); let templates = source.pad_template_list(); if templates .iter() .any(|templ| templ.presence() == gst::PadPresence::Request) { gst::error!(CAT, imp = self, "Request pads not supported"); gst::element_imp_error!( self, gst::LibraryError::Settings, ["Request pads not supported"] ); return Err(gst::StateChangeError); } let has_sometimes_pads = templates .iter() .any(|templ| templ.presence() == gst::PadPresence::Sometimes); // Handle all source pads that already exist for pad in source.src_pads() { if let Err(msg) = self.handle_source_pad_added(&pad) { self.post_error_message(msg); return Err(gst::StateChangeError); } } if !has_sometimes_pads { self.handle_source_no_more_pads(); } else { gst::debug!(CAT, imp = self, "Found sometimes pads"); let pad_added_sig_id = source.connect_pad_added(move |source, pad| { let element = match source .parent() .and_then(|p| p.downcast::().ok()) { Some(element) => element, None => return, }; let src = element.imp(); if let Err(msg) = src.handle_source_pad_added(pad) { element.post_error_message(msg); } }); let pad_removed_sig_id = source.connect_pad_removed(move |source, pad| { let element = match source .parent() .and_then(|p| p.downcast::().ok()) { Some(element) => element, None => return, }; let src = element.imp(); src.handle_source_pad_removed(pad); }); let no_more_pads_sig_id = source.connect_no_more_pads(move |source| { let element = match source .parent() .and_then(|p| p.downcast::().ok()) { Some(element) => element, None => return, }; let src = element.imp(); src.handle_source_no_more_pads(); }); let mut state = self.state.lock().unwrap(); state.pad_added_sig_id = Some(pad_added_sig_id); state.pad_removed_sig_id = Some(pad_removed_sig_id); state.no_more_pads_sig_id = Some(no_more_pads_sig_id); } Ok(gst::StateChangeSuccess::Success) } fn handle_source_pad_added(&self, pad: &gst::Pad) -> Result<(), gst::ErrorMessage> { gst::debug!(CAT, imp = self, "Source added pad {}", pad.name()); let mut state = self.state.lock().unwrap(); let (mut stream_type, mut stream_id) = (None, None); // Take stream type from stream-start event if we can if let Some(ev) = pad.sticky_event::(0) { stream_type = ev.stream().map(|s| s.stream_type()); stream_id = ev.stream().and_then(|s| s.stream_id()); } // Otherwise from the caps if stream_type.is_none() { let caps = match pad.current_caps().unwrap_or_else(|| pad.query_caps(None)) { caps if !caps.is_any() && !caps.is_empty() => caps, _ => { gst::error!(CAT, imp = self, "Pad {} had no caps", pad.name()); return Err(gst::error_msg!( gst::CoreError::Negotiation, ["Pad had no caps"] )); } }; let s = caps.structure(0).unwrap(); if s.name().starts_with("audio/") { stream_type = Some(gst::StreamType::AUDIO); } else if s.name().starts_with("video/") { stream_type = Some(gst::StreamType::VIDEO); } else { return Ok(()); } } let stream_type = stream_type.unwrap(); let (templ, name) = if stream_type.contains(gst::StreamType::AUDIO) { let name = format!("audio_{}", state.num_audio); state.num_audio += 1; (self.obj().pad_template("audio_%u").unwrap(), name) } else { let name = format!("video_{}", state.num_video); state.num_video += 1; (self.obj().pad_template("video_%u").unwrap(), name) }; let ghost_pad = gst::GhostPad::builder_from_template_with_target(&templ, pad) .unwrap() .name(name) .build(); ghost_pad.set_active(true).unwrap(); // If source posted a stream collection, we can(?) assume that the stream has an ID // Otherwise we create our own simple collection if !state.is_passthrough() { stream_id = if stream_type.contains(gst::StreamType::AUDIO) { Some(format!("{}/audio/{}", state.stream_id_prefix, state.num_audio - 1).into()) } else { Some(format!("{}/video/{}", state.stream_id_prefix, state.num_video - 1).into()) }; } else { assert!(stream_id.is_some()); } let expose_pad = state.is_passthrough(); let gst_stream = gst::Stream::new( Some(stream_id.as_ref().unwrap()), None, stream_type, gst::StreamFlags::empty(), ); let stream = Stream { source_pad: pad.clone(), ghost_pad: ghost_pad.clone().upcast(), stream: gst_stream.clone(), is_selected: true, }; state.pads.push(stream); drop(state); if expose_pad { let stream_start_event = gst::event::StreamStart::builder(&stream_id.unwrap()) .stream(gst_stream) .build(); ghost_pad.store_sticky_event(&stream_start_event).unwrap(); self.obj().add_pad(&ghost_pad).unwrap(); } Ok(()) } fn handle_source_pad_removed(&self, pad: &gst::Pad) { gst::debug!(CAT, imp = self, "Source removed pad {}", pad.name()); let mut state = self.state.lock().unwrap(); let (i, stream) = match state .pads .iter() .enumerate() .find(|(_i, p)| &p.source_pad == pad) { None => return, Some(v) => v, }; // If we're in streams-aware mode (have a collection from source) // then this is fine, probably happens because streams were de-selected. // Otherwise if the source is not stream-aware, this means the stream disappeared // and we need to remove it from our proxy collection. let (ghost_pad, is_exposed) = (stream.ghost_pad.clone(), stream.is_exposed()); state.pads.remove(i); if !state.is_passthrough() { let our_collection = state.our_collection(); let our_seqnum = gst::Seqnum::next(); state.selection_seqnum = Some(our_seqnum); drop(state); let _ = self.obj().post_message( gst::message::StreamsSelected::builder(&our_collection) .src(&*self.obj()) .build(), ); let state = self.state.lock().unwrap(); if state.selection_seqnum == Some(our_seqnum) { let selected_ids = state .pads .iter() .filter(|p| p.is_selected) .map(|p| p.stream.stream_id().unwrap()) .collect::>(); drop(state); if let Some(message) = self.handle_stream_selection(selected_ids) { let _ = self.obj().post_message(message); } } } else { drop(state); } if is_exposed { ghost_pad.set_active(false).unwrap(); let _ = ghost_pad.set_target(None::<&gst::Pad>); let _ = self.obj().remove_pad(&ghost_pad); } } fn handle_source_no_more_pads(&self) { gst::debug!(CAT, imp = self, "Source signalled no-more-pads"); let mut state = self.state.lock().unwrap(); // Make sure this isn't happening if a source posted a stream collection assert!(!state.is_passthrough()); // Tells ready->paused handler to post collection and handle selection there if !state.should_post_collection { state.should_post_collection = true; return; } self.post_collection(state); } fn post_collection(&self, mut state: MutexGuard) { let collection = state.our_collection(); let our_seqnum = gst::Seqnum::next(); state.selection_seqnum = Some(our_seqnum); state.should_post_collection = false; drop(state); let _ = self.obj().post_message( gst::message::StreamCollection::builder(&collection) .src(&*self.obj()) .build(), ); let state = self.state.lock().unwrap(); if state.selection_seqnum == Some(our_seqnum) { // Exposes all available pads by default let selected_ids = state .pads .iter() .map(|p| p.stream.stream_id().unwrap()) .collect::>(); drop(state); if let Some(message) = self.handle_stream_selection(selected_ids) { let _ = self.obj().post_message(message); } } } fn handle_stream_selection(&self, stream_ids: Vec) -> Option { let mut state_guard = self.state.lock().unwrap(); let state = &mut *state_guard; for id in stream_ids.iter() { if !state .pads .iter() .any(|p| p.stream.stream_id().unwrap() == *id) { gst::error!(CAT, imp = self, "Stream with ID {} not found!", id); return None; } } let mut selected_streams = vec![]; for stream in state.pads.iter_mut() { if stream_ids.contains(&stream.stream.stream_id().unwrap()) { stream.is_selected = true; selected_streams.push(stream.stream.clone()); gst::log!( CAT, imp = self, "Stream {} selected", stream.stream.stream_id().unwrap() ); } else { stream.is_selected = false; gst::log!( CAT, imp = self, "Stream {} not selected", stream.stream.stream_id().unwrap() ); } } let our_collection = state.our_collection(); let message = gst::message::StreamsSelected::builder(&our_collection) .streams(selected_streams) .src(&*self.obj()) .build(); self.expose_only_selected_streams(state_guard); Some(message) } fn expose_only_selected_streams(&self, state: MutexGuard) { let mut to_add = vec![]; let mut to_remove = vec![]; for stream in state.pads.iter() { if stream.is_selected && !stream.is_exposed() { let event = gst::event::StreamStart::builder(&stream.stream.stream_id().unwrap()) .stream(stream.stream.clone()) .build(); stream.ghost_pad.store_sticky_event(&event).unwrap(); to_add.push(stream.ghost_pad.clone()); } else if !stream.is_selected && stream.is_exposed() { let _ = stream.ghost_pad.set_target(None::<&gst::Pad>); to_remove.push(stream.ghost_pad.clone()); } } drop(state); for pad in to_add { self.obj().add_pad(&pad).unwrap(); } for pad in to_remove { let _ = self.obj().remove_pad(&pad); } } fn stop(&self) { gst::debug!(CAT, imp = self, "Stopping"); let mut state = self.state.lock().unwrap(); let source = self.source.get().unwrap(); if let Some(id) = state.pad_added_sig_id.take() { source.disconnect(id) } if let Some(id) = state.pad_removed_sig_id.take() { source.disconnect(id) } if let Some(id) = state.no_more_pads_sig_id.take() { source.disconnect(id) } let pads = mem::take(&mut state.pads); state.num_audio = 0; state.num_video = 0; state.received_collection = None; drop(state); for pad in pads.iter().filter(|s| s.is_exposed()) { let _ = pad.ghost_pad.set_target(None::<&gst::Pad>); let _ = self.obj().remove_pad(&pad.ghost_pad); } } } gst-plugin-fallbackswitch-0.14.3/src/fallbacksrc/custom_source/mod.rs000064400000000000000000000012321046102023000240340ustar 00000000000000// Copyright (C) 2020 Sebastian Dröge // // This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. // If a copy of the MPL was not distributed with this file, You can obtain one at // . // // SPDX-License-Identifier: MPL-2.0 use gst::glib; mod imp; glib::wrapper! { pub struct CustomSource(ObjectSubclass) @extends gst::Bin, gst::Element, gst::Object; } impl CustomSource { pub fn new(source: &gst::Element) -> CustomSource { gst::Object::builder() .property("source", source) .build() .unwrap() } } gst-plugin-fallbackswitch-0.14.3/src/fallbacksrc/imp.rs000064400000000000000000004736621046102023000211740ustar 00000000000000// Copyright (C) 2020 Sebastian Dröge // // This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. // If a copy of the MPL was not distributed with this file, You can obtain one at // . // // SPDX-License-Identifier: MPL-2.0 use gst::glib; use gst::prelude::*; use gst::subclass::prelude::*; use parking_lot::Condvar; use parking_lot::Mutex; use std::sync::Arc; use std::time::Instant; use std::{cmp, mem}; use std::sync::LazyLock; use super::custom_source::CustomSource; use super::{RetryReason, Status}; static CAT: LazyLock = LazyLock::new(|| { gst::DebugCategory::new( "fallbacksrc", gst::DebugColorFlags::empty(), Some("Fallback Source Bin"), ) }); #[derive(Debug, Clone)] struct Stats { num_retry: u64, num_fallback_retry: u64, last_retry_reason: RetryReason, last_fallback_retry_reason: RetryReason, buffering_percent: i32, fallback_buffering_percent: i32, } impl Default for Stats { fn default() -> Self { Self { num_retry: 0, num_fallback_retry: 0, last_retry_reason: RetryReason::None, last_fallback_retry_reason: RetryReason::None, buffering_percent: 100, fallback_buffering_percent: 100, } } } impl Stats { fn to_structure(&self) -> gst::Structure { gst::Structure::builder("application/x-fallbacksrc-stats") .field("num-retry", self.num_retry) .field("num-fallback-retry", self.num_fallback_retry) .field("last-retry-reason", self.last_retry_reason) .field( "last-fallback-retry-reason", self.last_fallback_retry_reason, ) .field("buffering-percent", self.buffering_percent) .field( "fallback-buffering-percent", self.fallback_buffering_percent, ) .build() } } #[derive(Debug, Clone)] struct Settings { uri: Option, source: Option, fallback_uri: Option, timeout: gst::ClockTime, restart_timeout: gst::ClockTime, retry_timeout: gst::ClockTime, restart_on_eos: bool, min_latency: gst::ClockTime, buffer_duration: i64, immediate_fallback: bool, manual_unblock: bool, fallback_video_caps: gst::Caps, fallback_audio_caps: gst::Caps, enable_audio: bool, enable_video: bool, } impl Default for Settings { fn default() -> Self { Settings { uri: None, source: None, fallback_uri: None, timeout: 5.seconds(), restart_timeout: 5.seconds(), retry_timeout: 60.seconds(), restart_on_eos: false, min_latency: gst::ClockTime::ZERO, buffer_duration: -1, immediate_fallback: false, manual_unblock: false, fallback_video_caps: gst::Caps::new_any(), fallback_audio_caps: gst::Caps::new_any(), enable_audio: true, enable_video: true, } } } #[derive(Debug)] enum Source { Uri(String), Element(gst::Element), } // Blocking buffer pad probe on the source pads. Once blocked we have a running time for the // current buffer that can later be used for offsetting // // This is used for the initial offsetting after starting of the stream and for "pausing" when // buffering. struct Block { pad: gst::Pad, probe_id: gst::PadProbeId, qos_probe_id: gst::PadProbeId, running_time: Option, } struct OutputBranch { // source pad from actual source inside the source bin source_srcpad: gst::Pad, // blocking pad probe on the source pad of the source queue source_srcpad_block: Option, // event pad probe on the source pad of the source bin, listening for EOS eos_probe: Option, // other elements in the source bin before the ghostpad clocksync: gst::Element, converters: gst::Element, queue: gst::Element, // queue source pad, target pad of the source ghost pad queue_srcpad: gst::Pad, // Request pad on the fallbackswitch switch_pad: gst::Pad, // Ghost pad on SourceBin.bin ghostpad: gst::GhostPad, } // Connects one source pad with fallbackswitch and the corresponding fallback input struct Output { // Main stream and fallback stream branches to the fallback switch main_branch: Option, // If this does not exist then the fallbackswitch is connected directly to the dummy // audio/video sources fallback_branch: Option, // Dummy source bin if fallback stream fails or is not present at all dummy_source: gst::Bin, // fallbackswitch // fallbackswitch in the main bin, linked to the ghostpads above switch: gst::Element, // output source pad on the main bin, switch source pad is ghostpad target srcpad: gst::GhostPad, // filter caps for the fallback/dummy streams filter_caps: gst::Caps, } struct Stream { // Our internal stream which will be exposed to the outside world gst_stream: gst::Stream, main_id: Option, fallback_id: Option, // Output of this stream, if selected output: Option, /// Whether it's one of the initial two streams which we never remove the output for persistent: bool, } struct SourceBin { // uridecodebin3 or custom source element inside a bin. // // This bin would also contain imagefreeze, clocksync and queue elements as needed for the // outputs and would be connected via ghost pads to the fallbackswitch elements. bin: gst::Bin, // Actual source element, e.g. uridecodebin3 source: gst::Element, pending_restart: bool, running: bool, is_live: bool, is_image: bool, // For timing out the source and shutting it down to restart it restart_timeout: Option, // For restarting the source after shutting it down pending_restart_timeout: Option, // For failing completely if we didn't recover after the retry timeout retry_timeout: Option, // Whole stream collection posted by source posted_streams: Option, } struct State { source: SourceBin, fallback_source: Option, flow_combiner: gst_base::UniqueFlowCombiner, // Our stream collection streams: Vec, // Stream ID prefix stream_id_prefix: String, // Counters for the stream ids num_audio: usize, num_video: usize, // Counters for the output pads num_audio_pads: usize, num_video_pads: usize, last_buffering_update: Option, fallback_last_buffering_update: Option, // Configure settings settings: Settings, configured_source: Source, // Statistics stats: Stats, // When application is using the manual-unblock property manually_blocked: bool, // So that we don't schedule a restart when manually unblocking // and our source hasn't reached the required state schedule_restart_on_unblock: bool, // Group ID for all our output streams group_id: gst::GroupId, // Sequence number for all our srcpad events // Chosen randomly on start() and then changed on seek seqnum: gst::Seqnum, // Used to check if we handled any stream select events after posting our collection selection_seqnum: gst::Seqnum, } impl State { fn selected_streams(&self) -> impl Iterator { self.streams.iter().filter(|s| s.output.is_some()) } fn stream_collection(&self) -> gst::StreamCollection { let streams = self.streams.iter().map(|s| s.gst_stream.clone()); gst::StreamCollection::builder(None) .streams(streams) .build() } // Structure: { "our_stream_id": { "main": "main_stream_id", "fallback": "fallback_stream_id" } } fn stream_map(&self) -> gst::Structure { let mut map = gst::Structure::builder("fallbacksrc-stream-map"); for stream in &self.streams { let stream_map = gst::Structure::builder("bound-streams") .field("main", &stream.main_id) .field("fallback", &stream.fallback_id) .build(); map = map.field(stream.gst_stream.stream_id().unwrap(), stream_map); } map.build() } } #[derive(Default)] pub struct FallbackSrc { settings: Mutex, state: Mutex>, } #[glib::object_subclass] impl ObjectSubclass for FallbackSrc { const NAME: &'static str = "GstFallbackSrc"; type Type = super::FallbackSrc; type ParentType = gst::Bin; } impl ObjectImpl for FallbackSrc { fn properties() -> &'static [glib::ParamSpec] { static PROPERTIES: LazyLock> = LazyLock::new(|| { vec![ glib::ParamSpecBoolean::builder("enable-audio") .nick("Enable Audio (DEPRECATED)") .blurb("Enable the audio stream, this will output silence if there's no audio in the configured URI") .default_value(true) .mutable_ready() .build(), glib::ParamSpecBoolean::builder("enable-video") .nick("Enable Video (DEPRECATED)") .blurb("Enable the video stream, this will output black or the fallback video if there's no video in the configured URI") .default_value(true) .mutable_ready() .build(), glib::ParamSpecString::builder("uri") .nick("URI") .blurb("URI to use") .mutable_ready() .build(), glib::ParamSpecObject::builder::("source") .nick("Source") .blurb("Source to use instead of the URI") .mutable_ready() .build(), glib::ParamSpecString::builder("fallback-uri") .nick("Fallback URI") .blurb("Fallback URI to use for video in case the main stream doesn't work") .mutable_ready() .build(), glib::ParamSpecUInt64::builder("timeout") .nick("Timeout") .blurb("Timeout for switching to the fallback URI") .maximum(u64::MAX - 1) .default_value(5 * *gst::ClockTime::SECOND) .mutable_ready() .build(), glib::ParamSpecUInt64::builder("restart-timeout") .nick("Timeout") .blurb("Timeout for restarting an active source") .maximum(u64::MAX - 1) .default_value(5 * *gst::ClockTime::SECOND) .mutable_ready() .build(), glib::ParamSpecUInt64::builder("retry-timeout") .nick("Retry Timeout") .blurb("Timeout for stopping after repeated failure") .maximum(u64::MAX - 1) .default_value(60 * *gst::ClockTime::SECOND) .mutable_ready() .build(), glib::ParamSpecBoolean::builder("restart-on-eos") .nick("Restart on EOS") .blurb("Restart source on EOS") .default_value(false) .mutable_ready() .build(), glib::ParamSpecEnum::builder_with_default("status", Status::Stopped) .nick("Status") .blurb("Current source status") .read_only() .build(), glib::ParamSpecUInt64::builder("min-latency") .nick("Minimum Latency") .blurb("When the main source has a higher latency than the fallback source \ this allows to configure a minimum latency that would be configured \ if initially the fallback is enabled") .maximum(u64::MAX - 1) .mutable_ready() .build(), glib::ParamSpecInt64::builder("buffer-duration") .nick("Buffer Duration") .blurb("Buffer duration when buffering streams (-1 default value)") .minimum(-1) .maximum(i64::MAX - 1) .default_value(-1) .mutable_ready() .build(), glib::ParamSpecBoxed::builder::("statistics") .nick("Statistics") .blurb("Various statistics") .read_only() .build(), glib::ParamSpecBoolean::builder("manual-unblock") .nick("Manual unblock") .blurb("When enabled, the application must call the unblock signal, except for live streams") .default_value(false) .mutable_ready() .build(), glib::ParamSpecBoolean::builder("immediate-fallback") .nick("Immediate fallback") .blurb("Forward the fallback streams immediately at startup, when the primary streams are slow to start up and immediate output is required") .default_value(false) .mutable_ready() .build(), glib::ParamSpecBoxed::builder::("fallback-video-caps") .nick("Fallback Video Caps") .blurb("Raw video caps for fallback stream") .mutable_ready() .build(), glib::ParamSpecBoxed::builder::("fallback-audio-caps") .nick("Fallback Audio Caps") .blurb("Raw audio caps for fallback stream") .mutable_ready() .build(), ] }); PROPERTIES.as_ref() } fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { match pspec.name() { "enable-audio" => { let mut settings = self.settings.lock(); let new_value = value.get().expect("type checked upstream"); gst::info!( CAT, imp = self, "Changing enable-audio from {:?} to {:?}", settings.enable_audio, new_value, ); settings.enable_audio = new_value; } "enable-video" => { let mut settings = self.settings.lock(); let new_value = value.get().expect("type checked upstream"); gst::info!( CAT, imp = self, "Changing enable-video from {:?} to {:?}", settings.enable_video, new_value, ); settings.enable_video = new_value; } "uri" => { let mut settings = self.settings.lock(); let new_value = value.get().expect("type checked upstream"); gst::info!( CAT, imp = self, "Changing URI from {:?} to {:?}", settings.uri, new_value, ); settings.uri = new_value; } "source" => { let mut settings = self.settings.lock(); let new_value = value.get().expect("type checked upstream"); gst::info!( CAT, imp = self, "Changing source from {:?} to {:?}", settings.source, new_value, ); settings.source = new_value; } "fallback-uri" => { let mut settings = self.settings.lock(); let new_value = value.get().expect("type checked upstream"); gst::info!( CAT, imp = self, "Changing Fallback URI from {:?} to {:?}", settings.fallback_uri, new_value, ); settings.fallback_uri = new_value; } "timeout" => { let mut settings = self.settings.lock(); let new_value = value.get().expect("type checked upstream"); gst::info!( CAT, imp = self, "Changing timeout from {:?} to {:?}", settings.timeout, new_value, ); settings.timeout = new_value; } "restart-timeout" => { let mut settings = self.settings.lock(); let new_value = value.get().expect("type checked upstream"); gst::info!( CAT, imp = self, "Changing Restart Timeout from {:?} to {:?}", settings.restart_timeout, new_value, ); settings.restart_timeout = new_value; } "retry-timeout" => { let mut settings = self.settings.lock(); let new_value = value.get().expect("type checked upstream"); gst::info!( CAT, imp = self, "Changing Retry Timeout from {:?} to {:?}", settings.retry_timeout, new_value, ); settings.retry_timeout = new_value; } "restart-on-eos" => { let mut settings = self.settings.lock(); let new_value = value.get().expect("type checked upstream"); gst::info!( CAT, imp = self, "Changing restart-on-eos from {:?} to {:?}", settings.restart_on_eos, new_value, ); settings.restart_on_eos = new_value; } "min-latency" => { let mut settings = self.settings.lock(); let new_value = value.get().expect("type checked upstream"); gst::info!( CAT, imp = self, "Changing Minimum Latency from {:?} to {:?}", settings.min_latency, new_value, ); settings.min_latency = new_value; } "buffer-duration" => { let mut settings = self.settings.lock(); let new_value = value.get().expect("type checked upstream"); gst::info!( CAT, imp = self, "Changing Buffer Duration from {:?} to {:?}", settings.buffer_duration, new_value, ); settings.buffer_duration = new_value; } "immediate-fallback" => { let mut settings = self.settings.lock(); let new_value = value.get().expect("type checked upstream"); gst::info!( CAT, imp = self, "Changing immediate-fallback from {:?} to {:?}", settings.immediate_fallback, new_value, ); settings.immediate_fallback = new_value; } "manual-unblock" => { let mut settings = self.settings.lock(); let new_value = value.get().expect("type checked upstream"); gst::info!( CAT, imp = self, "Changing manual-unblock from {:?} to {:?}", settings.manual_unblock, new_value, ); settings.manual_unblock = new_value; } "fallback-video-caps" => { let mut settings = self.settings.lock(); let new_value = value .get::>() .expect("type checked upstream") .unwrap_or_else(gst::Caps::new_any); gst::info!( CAT, imp = self, "Changing fallback video caps from {} to {}", settings.fallback_video_caps, new_value, ); settings.fallback_video_caps = new_value; } "fallback-audio-caps" => { let mut settings = self.settings.lock(); let new_value = value .get::>() .expect("type checked upstream") .unwrap_or_else(gst::Caps::new_any); gst::info!( CAT, imp = self, "Changing fallback audio caps from {} to {}", settings.fallback_audio_caps, new_value, ); settings.fallback_audio_caps = new_value; } _ => unimplemented!(), } } // Called whenever a value of a property is read. It can be called // at any time from any thread. #[allow(clippy::blocks_in_conditions)] fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { match pspec.name() { "enable-audio" => { let settings = self.settings.lock(); settings.enable_audio.to_value() } "enable-video" => { let settings = self.settings.lock(); settings.enable_video.to_value() } "uri" => { let settings = self.settings.lock(); settings.uri.to_value() } "source" => { let settings = self.settings.lock(); settings.source.to_value() } "fallback-uri" => { let settings = self.settings.lock(); settings.fallback_uri.to_value() } "timeout" => { let settings = self.settings.lock(); settings.timeout.to_value() } "restart-timeout" => { let settings = self.settings.lock(); settings.restart_timeout.to_value() } "retry-timeout" => { let settings = self.settings.lock(); settings.retry_timeout.to_value() } "restart-on-eos" => { let settings = self.settings.lock(); settings.restart_on_eos.to_value() } "status" => { let state_guard = self.state.lock(); // If we have no state then we're stopped let state = match &*state_guard { None => return Status::Stopped.to_value(), Some(ref state) => state, }; if !state.source.running { return Status::Stopped.to_value(); } // If any restarts/retries are pending, we're retrying if state.source.pending_restart || state.source.pending_restart_timeout.is_some() || state.source.retry_timeout.is_some() { return Status::Retrying.to_value(); } // Otherwise if buffering < 100, we have no streams yet or of the expected // streams not all have their source pad yet, we're buffering if state.stats.buffering_percent < 100 || state.source.restart_timeout.is_some() || state.source.posted_streams.is_none() { return Status::Buffering.to_value(); } // Output present means a stream was chosen: // If its main branch is missing, we didn't get a srcpad for it yet. // If we did but it's blocked, at least one other stream is still missing a srcpad. // In both cases we're still buffering. if state .streams .iter() .filter_map(|s| s.output.as_ref()) .map(|o| o.main_branch.as_ref()) .any(|b| b.map(|b| b.source_srcpad_block.is_some()).unwrap_or(true)) { return Status::Buffering.to_value(); } // Otherwise we're running now Status::Running.to_value() } "min-latency" => { let settings = self.settings.lock(); settings.min_latency.to_value() } "buffer-duration" => { let settings = self.settings.lock(); settings.buffer_duration.to_value() } "statistics" => self.stats().to_value(), "immediate-fallback" => { let settings = self.settings.lock(); settings.immediate_fallback.to_value() } "manual-unblock" => { let settings = self.settings.lock(); settings.manual_unblock.to_value() } "fallback-video-caps" => { let settings = self.settings.lock(); settings.fallback_video_caps.to_value() } "fallback-audio-caps" => { let settings = self.settings.lock(); settings.fallback_audio_caps.to_value() } _ => unimplemented!(), } } fn signals() -> &'static [glib::subclass::Signal] { static SIGNALS: LazyLock> = LazyLock::new(|| { vec![ glib::subclass::Signal::builder("update-uri") .param_types([String::static_type()]) .return_type::() .class_handler(|args| { // Simply return the input by default Some(args[1].clone()) }) .accumulator(|_hint, _acc, value| { // First signal handler wins std::ops::ControlFlow::Break(value.clone()) }) .build(), glib::subclass::Signal::builder("unblock") .action() .class_handler(|args| { let element = args[0].get::().expect("signal arg"); let imp = element.imp(); let mut state_guard = imp.state.lock(); let state = match &mut *state_guard { None => { return None; } Some(state) => state, }; state.manually_blocked = false; if state.schedule_restart_on_unblock && imp.all_pads_fallback_activated(state) { imp.schedule_source_restart_timeout(state, gst::ClockTime::ZERO, false); } imp.unblock_pads(state, false); None }) .build(), /** * GstFallbackSrc::map-streams: * @map: A #GstStructure containing the map of our streams to main and fallback streams * @main_collection: (nullable): A #GstStreamCollection posted by the main source * @fallback_collection: (nullable): A #GstStreamCollection posted by the fallback source * * The @map contains the following structure: * { "our_stream_id": { "main": "main_stream_id", "fallback": "fallback_stream_id" } } * * Consumers should only change the fallback IDs and leave other fields untouched. * If any other changes are detected, the new map will be ignored. * If the current mapping is fine, the map should be returned as is. * * This signal is emitted when either the fallback source posts a new stream collection, * or the main source posts a new stream collection after the fallback source has posted one. * Internally, streams will be relinked immediately after this signal is handled. * * Returns: An updated #GstStructure containing the mapping of our stream IDs to main and fallback stream IDs. */ glib::subclass::Signal::builder("map-streams") .param_types([ gst::Structure::static_type(), gst::StreamCollection::static_type(), gst::StreamCollection::static_type(), ]) .return_type::() .build(), ] }); SIGNALS.as_ref() } fn constructed(&self) { self.parent_constructed(); let obj = self.obj(); obj.set_suppressed_flags(gst::ElementFlags::SOURCE | gst::ElementFlags::SINK); obj.set_element_flags(gst::ElementFlags::SOURCE); obj.set_bin_flags(gst::BinFlags::STREAMS_AWARE); } } impl GstObjectImpl for FallbackSrc {} impl ElementImpl for FallbackSrc { fn metadata() -> Option<&'static gst::subclass::ElementMetadata> { static ELEMENT_METADATA: LazyLock = LazyLock::new(|| { #[cfg(feature = "doc")] Status::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty()); gst::subclass::ElementMetadata::new( "Fallback Source", "Generic/Source", "Live source with uridecodebin3 or custom source, and fallback stream", "Sebastian Dröge ", ) }); Some(&*ELEMENT_METADATA) } fn pad_templates() -> &'static [gst::PadTemplate] { static PAD_TEMPLATES: LazyLock> = LazyLock::new(|| { let audio_src_pad_template = gst::PadTemplate::new( "audio_%u", gst::PadDirection::Src, gst::PadPresence::Sometimes, &gst::Caps::new_any(), ) .unwrap(); let video_src_pad_template = gst::PadTemplate::new( "video_%u", gst::PadDirection::Src, gst::PadPresence::Sometimes, &gst::Caps::new_any(), ) .unwrap(); vec![audio_src_pad_template, video_src_pad_template] }); PAD_TEMPLATES.as_ref() } #[allow(clippy::single_match)] fn change_state( &self, transition: gst::StateChange, ) -> Result { gst::debug!(CAT, imp = self, "Changing state {:?}", transition); match transition { gst::StateChange::NullToReady => { self.start()?; } gst::StateChange::ReadyToPaused => { self.post_initial_collection()?; if let Some(parent) = self.obj().parent() { if let Some(bin) = parent.downcast_ref::() { if !bin.bin_flags().contains(gst::BinFlags::STREAMS_AWARE) { self.obj().no_more_pads(); } } } } _ => (), } self.parent_change_state(transition).inspect_err(|_err| { gst::error!( CAT, imp = self, "Parent state change transition {:?} failed", transition ); })?; // Change the source state manually here to be able to catch errors. State changes always // happen from sink to source, so we do this after chaining up. self.change_source_state(transition, false); // Change the fallback source state manually here to be able to catch errors. State changes always // happen from sink to source, so we do this after chaining up. self.change_source_state(transition, true); // Ignore parent state change return to prevent spurious async/no-preroll return values // due to core state change bugs match transition { gst::StateChange::ReadyToPaused | gst::StateChange::PausedToPaused | gst::StateChange::PlayingToPaused => Ok(gst::StateChangeSuccess::NoPreroll), gst::StateChange::ReadyToNull => { self.stop(); Ok(gst::StateChangeSuccess::Success) } _ => Ok(gst::StateChangeSuccess::Success), } } fn send_event(&self, event: gst::Event) -> bool { match event.view() { gst::EventView::SelectStreams(e) => { gst::debug!(CAT, imp = self, "Handling stream selection event"); self.handle_select_stream_event(e) } gst::EventView::Eos(..) => { gst::debug!( CAT, imp = self, "Handling element-level EOS, forwarding to all streams" ); let mut state_guard = self.state.lock(); let state = match &mut *state_guard { None => { return true; } Some(state) => state, }; // We don't want to hold the state lock while pushing out EOS let mut send_eos_elements = vec![]; let mut send_eos_pads = vec![]; send_eos_elements.push(state.source.bin.clone()); // Not strictly necessary as the switch will EOS when receiving // EOS on its primary pad, just good form. if let Some(ref source) = state.fallback_source { send_eos_elements.push(source.bin.clone()); } for output in state.streams.iter().filter_map(|s| s.output.as_ref()) { send_eos_elements.push(output.dummy_source.clone()); for branch in [output.main_branch.as_ref(), output.fallback_branch.as_ref()] .iter() .flatten() { send_eos_pads.push(branch.queue.static_pad("sink").unwrap()); } } drop(state_guard); for elem in send_eos_elements { elem.send_event(event.clone()); } for pad in send_eos_pads { pad.send_event(event.clone()); } true } _ => true, } } } impl BinImpl for FallbackSrc { fn handle_message(&self, msg: gst::Message) { use gst::MessageView; match msg.view() { MessageView::Buffering(m) => { // Don't forward upwards, we handle this internally self.handle_buffering(m); } MessageView::StreamsSelected(m) => { // Don't forward upwards, we have internal mapping between our and source streams self.handle_streams_selected(m); } MessageView::StreamCollection(m) => { self.handle_stream_collection(m); } MessageView::Error(m) => { if !self.handle_error(m) { self.parent_handle_message(msg); } } _ => self.parent_handle_message(msg), } } } impl FallbackSrc { fn create_dummy_audio_source(filter_caps: &gst::Caps, min_latency: gst::ClockTime) -> gst::Bin { let bin = gst::Bin::default(); let audiotestsrc = gst::ElementFactory::make("audiotestsrc") .name("audiosrc") .property_from_str("wave", "silence") .property("is-live", true) .build() .expect("No audiotestsrc found"); let audioconvert = gst::ElementFactory::make("audioconvert") .name("audio_audioconvert") .build() .expect("No audioconvert found"); let audioresample = gst::ElementFactory::make("audioresample") .name("audio_audioresample") .build() .expect("No audioresample found"); let capsfilter = gst::ElementFactory::make("capsfilter") .name("audio_capsfilter") .property("caps", filter_caps) .build() .expect("No capsfilter found"); let queue = gst::ElementFactory::make("queue") .property("max-size-bytes", 0u32) .property("max-size-buffers", 0u32) .property("max-size-time", cmp::max(min_latency, 1.seconds())) .build() .expect("No queue found"); bin.add_many([ &audiotestsrc, &audioconvert, &audioresample, &capsfilter, &queue, ]) .unwrap(); gst::Element::link_many([ &audiotestsrc, &audioconvert, &audioresample, &capsfilter, &queue, ]) .unwrap(); let ghostpad = gst::GhostPad::with_target(&queue.static_pad("src").unwrap()).unwrap(); ghostpad.set_active(true).unwrap(); bin.add_pad(&ghostpad).unwrap(); bin } fn create_dummy_video_source(filter_caps: &gst::Caps, min_latency: gst::ClockTime) -> gst::Bin { let bin = gst::Bin::default(); let videotestsrc = gst::ElementFactory::make("videotestsrc") .name("videosrc") .property_from_str("pattern", "black") .property("is-live", true) .build() .expect("No videotestsrc found"); let videoconvert = gst::ElementFactory::make("videoconvert") .name("video_videoconvert") .build() .expect("No videoconvert found"); let videoscale = gst::ElementFactory::make("videoscale") .name("video_videoscale") .build() .expect("No videoscale found"); let capsfilter = gst::ElementFactory::make("capsfilter") .name("video_capsfilter") .property("caps", filter_caps) .build() .expect("No capsfilter found"); let queue = gst::ElementFactory::make("queue") .property("max-size-bytes", 0u32) .property("max-size-buffers", 0u32) .property("max-size-time", cmp::max(min_latency, 1.seconds())) .build() .expect("No queue found"); bin.add_many([ &videotestsrc, &videoconvert, &videoscale, &capsfilter, &queue, ]) .unwrap(); gst::Element::link_many([ &videotestsrc, &videoconvert, &videoscale, &capsfilter, &queue, ]) .unwrap(); let ghostpad = gst::GhostPad::with_target(&queue.static_pad("src").unwrap()).unwrap(); ghostpad.set_active(true).unwrap(); bin.add_pad(&ghostpad).unwrap(); bin } fn create_main_input(&self, source: &Source, buffer_duration: i64) -> SourceBin { let bin = gst::Bin::default(); let source = match source { Source::Uri(ref uri) => { let uri = self .obj() .emit_by_name::("update-uri", &[uri]); let source = gst::ElementFactory::make("uridecodebin3") .name("dbin-main") .property("uri", uri) .property("use-buffering", true) .property("buffer-duration", buffer_duration) .build() .expect("No uridecodebin3 found"); source } Source::Element(ref source) => CustomSource::new(source).upcast(), }; bin.add(&source).unwrap(); // Handle any async state changes internally, they don't affect the pipeline because we // convert everything to a live stream bin.set_property("async-handling", true); // Don't let the bin handle state changes of the source. We want to do it manually to catch // possible errors and retry, without causing the whole bin state change to fail bin.set_locked_state(true); source.connect_pad_added(move |source, pad| { let element = match source .parent() .and_then(|p| p.parent()) .and_then(|p| p.downcast::().ok()) { None => return, Some(element) => element, }; let imp = element.imp(); if let Err(msg) = imp.handle_source_pad_added(pad, false) { element.post_error_message(msg); } }); source.connect_pad_removed(move |source, pad| { let element = match source .parent() .and_then(|p| p.parent()) .and_then(|p| p.downcast::().ok()) { None => return, Some(element) => element, }; let imp = element.imp(); imp.handle_source_pad_removed(pad, false); }); self.obj().add(&bin).unwrap(); SourceBin { bin, source, pending_restart: false, running: false, is_live: false, is_image: false, restart_timeout: None, pending_restart_timeout: None, retry_timeout: None, posted_streams: None, } } fn create_fallback_input( &self, fallback_uri: Option<&str>, buffer_duration: i64, ) -> Option { let source: gst::Element = match fallback_uri { Some(uri) => { let dbin = gst::ElementFactory::make("uridecodebin3") .name("dbin-fallback") .property("uri", uri) .property("use-buffering", true) .property("buffer-duration", buffer_duration) .build() .expect("No uridecodebin3 found"); dbin } None => return None, }; let bin = gst::Bin::default(); bin.add(&source).unwrap(); source.connect_pad_added(move |source, pad| { let element = match source .parent() .and_then(|p| p.parent()) .and_then(|p| p.downcast::().ok()) { None => return, Some(element) => element, }; let imp = element.imp(); if let Err(msg) = imp.handle_source_pad_added(pad, true) { element.post_error_message(msg); } }); source.connect_pad_removed(move |source, pad| { let element = match source .parent() .and_then(|p| p.parent()) .and_then(|p| p.downcast::().ok()) { None => return, Some(element) => element, }; let src = element.imp(); src.handle_source_pad_removed(pad, true); }); // Handle any async state changes internally, they don't affect the pipeline because we // convert everything to a live stream bin.set_property("async-handling", true); // Don't let the bin handle state changes of the dbin. We want to do it manually to catch // possible errors and retry, without causing the whole bin state change to fail bin.set_locked_state(true); self.obj().add(&bin).unwrap(); Some(SourceBin { bin, source, pending_restart: false, running: false, is_live: false, is_image: false, restart_timeout: None, pending_restart_timeout: None, retry_timeout: None, posted_streams: None, }) } /// Creates a new stream together with its gst::Stream. /// /// Streams will be later mapped to individual streams of the main/fallback sources, and will /// get Outputs assigned if they were selected by the application. /// /// Initially one audio and one video stream are created, which are persistent. If the main /// source adds more streams of a kind then more streams are created here to map to them. fn create_stream( &self, state: &mut State, stream_type: gst::StreamType, is_persistent: bool, ) -> Stream { let (stream_id, caps) = match stream_type { gst::StreamType::AUDIO => { state.num_audio += 1; ( format!("{}/audio/{}", state.stream_id_prefix, state.num_audio - 1), gst::Caps::builder("audio/x-raw").any_features().build(), ) } gst::StreamType::VIDEO => { state.num_video += 1; ( format!("{}/video/{}", state.stream_id_prefix, state.num_video - 1), gst::Caps::builder("video/x-raw").any_features().build(), ) } _ => unreachable!(), }; let gst_stream = gst::Stream::new( Some(&stream_id), Some(&caps), stream_type, gst::StreamFlags::empty(), ); Stream { gst_stream: gst_stream.clone(), main_id: None, fallback_id: None, output: None, persistent: is_persistent, } } /// Creates a new output for the given stream. /// /// An output corresponds to a selected stream, and has an external source pad. /// /// Each output has its own dummy source for simplicity, which is linked to its /// fallbackswitch. /// /// Streams of the main/fallback sources which are mapped to this stream are connected to this /// output, and also linked to the fallbackswitch. #[allow(clippy::too_many_arguments)] fn create_output( &self, stream: &gst::Stream, timeout: gst::ClockTime, min_latency: gst::ClockTime, is_audio: bool, number: usize, immediate_fallback: bool, filter_caps: &gst::Caps, seqnum: gst::Seqnum, group_id: gst::GroupId, ) -> Output { let switch = gst::ElementFactory::make("fallbackswitch") .property("timeout", timeout.nseconds()) .property("min-upstream-latency", min_latency.nseconds()) .property("immediate-fallback", immediate_fallback) .build() .expect("No fallbackswitch found"); self.obj().add(&switch).unwrap(); switch.sync_state_with_parent().unwrap(); let dummy_source = if is_audio { Self::create_dummy_audio_source(filter_caps, min_latency) } else { Self::create_dummy_video_source(filter_caps, min_latency) }; self.obj().add(&dummy_source).unwrap(); dummy_source.sync_state_with_parent().unwrap(); let dummy_srcpad = dummy_source.static_pad("src").unwrap(); let dummy_sinkpad = switch.request_pad_simple("sink_%u").unwrap(); dummy_sinkpad.set_property("priority", 2u32); dummy_srcpad .link_full( &dummy_sinkpad, gst::PadLinkCheck::all() & !gst::PadLinkCheck::CAPS, ) .unwrap(); let stream_clone = stream.clone(); switch.connect_notify(Some("active-pad"), move |switch, _pspec| { let element = match switch .parent() .and_then(|p| p.downcast::().ok()) { None => return, Some(element) => element, }; let imp = element.imp(); imp.handle_switch_active_pad_change(&stream_clone); }); let srcpad = switch.static_pad("src").unwrap(); let templ = self .obj() .pad_template(if is_audio { "audio_%u" } else { "video_%u" }) .unwrap(); let name = format!("{}_{}", if is_audio { "audio" } else { "video" }, number); let ghostpad = gst::GhostPad::builder_from_template_with_target(&templ, &srcpad) .unwrap() .name(name.clone()) .proxy_pad_chain_function({ move |pad, parent, buffer| { let parent = parent.and_then(|p| p.parent()); FallbackSrc::catch_panic_pad_function( parent.as_ref(), || Err(gst::FlowError::Error), |imp| imp.proxy_pad_chain(pad, buffer), ) } }) .proxy_pad_event_function({ let stream = stream.clone(); move |pad, parent, event| { let parent = parent.and_then(|p| p.parent()); if parent.is_none() { return gst::Pad::event_default(pad, parent.as_ref(), event); } FallbackSrc::catch_panic_pad_function( parent.as_ref(), || false, |imp| imp.proxy_pad_event(&stream, pad, event), ) } }) .event_function({ move |pad, parent, event| { if parent.is_none() { return gst::Pad::event_default(pad, parent, event); } FallbackSrc::catch_panic_pad_function( parent, || false, |imp| imp.ghost_pad_event(pad, event), ) } }) .build(); // Directly store the stream-start event on the pad before adding so the application can // map it to the stream. let stream_id = stream.stream_id().unwrap(); let stream_start_event = gst::event::StreamStart::builder(&stream_id) .seqnum(seqnum) .group_id(group_id) .stream(stream.clone()) .build(); ghostpad.set_active(true).unwrap(); ghostpad.store_sticky_event(&stream_start_event).unwrap(); self.obj().add_pad(&ghostpad).unwrap(); Output { main_branch: None, fallback_branch: None, dummy_source, switch, srcpad: ghostpad.upcast(), filter_caps: filter_caps.clone(), } } /// Removes an output and all its elements. fn remove_output(&self, mut output: Output) { if let Some(ref mut branch) = output.main_branch { let source = branch .clocksync .parent() .and_downcast::() .unwrap(); self.handle_branch_teardown(&output.switch, &source, branch, false); } if let Some(ref mut branch) = output.fallback_branch { let source = branch .clocksync .parent() .and_downcast::() .unwrap(); self.handle_branch_teardown(&output.switch, &source, branch, true); } let element = self.obj(); output.switch.set_state(gst::State::Null).unwrap(); element.remove(&output.switch).unwrap(); output.dummy_source.set_state(gst::State::Null).unwrap(); element.remove(&output.dummy_source).unwrap(); let _ = output.srcpad.set_target(None::<&gst::Pad>); element.remove_pad(&output.srcpad).unwrap(); } /// Sets everything up, but doesn't create any outputs. /// That happens in ReadyToPaused, when post_initial_collection() is called. fn start(&self) -> Result<(), gst::StateChangeError> { gst::debug!(CAT, imp = self, "Starting"); let mut state_guard = self.state.lock(); if state_guard.is_some() { return Err(gst::StateChangeError); } let settings = self.settings.lock().clone(); let configured_source = match settings .uri .as_ref() .cloned() .map(Source::Uri) .or_else(|| settings.source.as_ref().cloned().map(Source::Element)) { Some(source) => source, None => { gst::error!(CAT, imp = self, "No URI or source element configured"); gst::element_imp_error!( self, gst::LibraryError::Settings, ["No URI or source element configured"] ); return Err(gst::StateChangeError); } }; let fallback_uri = &settings.fallback_uri; // Create main input let source = self.create_main_input(&configured_source, settings.buffer_duration); // Create fallback input let fallback_source = self.create_fallback_input(fallback_uri.as_deref(), settings.buffer_duration); let flow_combiner = gst_base::UniqueFlowCombiner::new(); let manually_blocked = settings.manual_unblock; let mut state = State { source, fallback_source, streams: vec![], stream_id_prefix: format!("{:016x}", rand::random::()), num_audio: 0, num_video: 0, num_audio_pads: 0, num_video_pads: 0, flow_combiner, last_buffering_update: None, fallback_last_buffering_update: None, settings, configured_source, stats: Stats::default(), manually_blocked, schedule_restart_on_unblock: false, group_id: gst::GroupId::next(), seqnum: gst::Seqnum::next(), selection_seqnum: gst::Seqnum::next(), }; // Always propose at least 1 video and 1 audio stream by default // (will just do fallback/dummy if main source doesn't provide either of those) // See post_initial_collection() which is called in ReadyToPaused let stream = self.create_stream(&mut state, gst::StreamType::AUDIO, true); state.streams.push(stream); let stream = self.create_stream(&mut state, gst::StreamType::VIDEO, true); state.streams.push(stream); *state_guard = Some(state); drop(state_guard); self.obj().notify("status"); gst::debug!(CAT, imp = self, "Started"); Ok(()) } /// Posts the initial StreamCollection with our two default streams. /// Either user selects no streams, so we go with the defaults and call perform_selection() /// ourselves here, or select-streams arrives and it's called there. /// Either way, StreamCollection and StreamsSelected messages will be posted as a result. fn post_initial_collection(&self) -> Result<(), gst::StateChangeError> { let mut state_guard = self.state.lock(); let state = match &mut *state_guard { None => { return Ok(()); } Some(state) => state, }; let collection = state.stream_collection(); assert_eq!(collection.size(), 2); let our_seqnum = state.selection_seqnum; drop(state_guard); gst::debug!(CAT, imp = self, "Posting initial StreamCollection"); let _ = self .obj() .post_message(gst::message::StreamCollection::builder(&collection).build()); state_guard = self.state.lock(); let state = match &mut *state_guard { None => { return Ok(()); } Some(state) => state, }; if state.selection_seqnum == our_seqnum { gst::debug!(CAT, imp = self, "Using default selection, creating outputs"); let settings = self.settings.lock(); let selected_ids = state .streams .iter() .filter(|s| { s.gst_stream.stream_type() == gst::StreamType::AUDIO && settings.enable_audio || s.gst_stream.stream_type() == gst::StreamType::VIDEO && settings.enable_video }) .map(|s| s.gst_stream.stream_id().unwrap()) .collect::>(); drop(settings); drop(state_guard); if let Some((msg, events)) = self.perform_selection(&selected_ids) { for (element, event) in events { if !element.send_event(event) { gst::error!( CAT, imp = self, "Sending select-streams to {} failed", element.name() ); return Err(gst::StateChangeError); } } gst::debug!( CAT, imp = self, "Posting streams-selected for default collection" ); let _ = self.obj().post_message(msg); } } else { gst::debug!( CAT, imp = self, "Stream selection handled while posting default StreamCollection" ); } Ok(()) } fn stop(&self) { gst::debug!(CAT, imp = self, "Stopping"); let mut state_guard = self.state.lock(); let mut state = match state_guard.take() { Some(state) => state, None => return, }; drop(state_guard); self.obj().notify("status"); // In theory all streams should've been removed from the source's pad-removed signal // handler when going from Paused to Ready but better safe than sorry here for output in state.streams.drain(..).filter_map(|s| s.output) { self.remove_output(output); } if let Source::Element(ref source) = state.configured_source { // Explicitly remove the source element from the CustomSource so that we can // later create a new CustomSource and add it again there. let parent = if source.has_as_parent(&state.source.bin) { Some(&state.source.bin) } else if source.has_as_parent(&state.source.source) { Some(state.source.source.downcast_ref::().unwrap()) } else { None }; if let Some(parent) = parent { let _ = source.set_state(gst::State::Null); let _ = parent.remove(source); } } for source in [Some(&mut state.source), state.fallback_source.as_mut()] .iter_mut() .flatten() { self.obj().remove(&source.bin).unwrap(); if let Some(timeout) = source.pending_restart_timeout.take() { timeout.unschedule(); } if let Some(timeout) = source.retry_timeout.take() { timeout.unschedule(); } if let Some(timeout) = source.restart_timeout.take() { timeout.unschedule(); } } gst::debug!(CAT, imp = self, "Stopped"); } fn change_source_state(&self, transition: gst::StateChange, fallback_source: bool) { gst::debug!( CAT, imp = self, "Changing {}source state: {:?}", if fallback_source { "fallback " } else { "" }, transition ); let mut state_guard = self.state.lock(); let state = match &mut *state_guard { Some(state) => state, None => return, }; let source = if fallback_source { if let Some(ref mut source) = state.fallback_source { source } else { return; } } else { &mut state.source }; source.running = transition.next() > gst::State::Ready; if transition.current() <= transition.next() && source.pending_restart { gst::debug!( CAT, imp = self, "Not starting {}source because pending restart", if fallback_source { "fallback " } else { "" } ); return; } else if transition.next() <= gst::State::Ready && source.pending_restart { gst::debug!( CAT, imp = self, "Unsetting pending {}restart because shutting down", if fallback_source { "fallback " } else { "" } ); source.pending_restart = false; if let Some(timeout) = source.pending_restart_timeout.take() { timeout.unschedule(); } } let source = source.bin.clone(); drop(state_guard); self.obj().notify("status"); let res = source.set_state(transition.next()); gst::debug!( CAT, imp = self, "{}source changing state: {:?}", if fallback_source { "fallback " } else { "" }, res ); match res { Err(_) => { gst::error!( CAT, imp = self, "{}source failed to change state", if fallback_source { "fallback " } else { "" } ); // Try again later if we're not shutting down if transition != gst::StateChange::ReadyToNull { let _ = source.set_state(gst::State::Null); let mut state_guard = self.state.lock(); let Some(ref mut state) = &mut *state_guard else { return; }; self.handle_source_error( state, RetryReason::StateChangeFailure, fallback_source, ); drop(state_guard); self.obj().notify("statistics"); } } Ok(res) => { gst::debug!( CAT, imp = self, "{}source changed state successfully: {:?}", if fallback_source { "fallback " } else { "" }, res ); let mut state_guard = self.state.lock(); let Some(ref mut state) = &mut *state_guard else { return; }; let source = if fallback_source { if let Some(ref mut source) = state.fallback_source { source } else { return; } } else { &mut state.source }; // Remember if the source is live if transition == gst::StateChange::ReadyToPaused { source.is_live = res == gst::StateChangeSuccess::NoPreroll; } if (!source.is_live && transition == gst::StateChange::ReadyToPaused) || (source.is_live && transition == gst::StateChange::PausedToPlaying) { if !fallback_source { state.schedule_restart_on_unblock = true; } if source.restart_timeout.is_none() { self.schedule_source_restart_timeout( state, gst::ClockTime::ZERO, fallback_source, ); } } else if (!source.is_live && transition == gst::StateChange::PausedToReady) || (source.is_live && transition == gst::StateChange::PlayingToPaused) { if let Some(timeout) = source.pending_restart_timeout.take() { timeout.unschedule(); } if let Some(timeout) = source.retry_timeout.take() { timeout.unschedule(); } if let Some(timeout) = source.restart_timeout.take() { timeout.unschedule(); } } } } } #[allow(clippy::single_match)] fn proxy_pad_event( &self, stream: &gst::Stream, pad: &gst::ProxyPad, mut event: gst::Event, ) -> bool { match event.view() { gst::EventView::StreamStart(_) => { let stream_id = stream.stream_id().unwrap(); let mut state_guard = self.state.lock(); let state = match &mut *state_guard { None => return false, Some(state) => state, }; event = gst::event::StreamStart::builder(&stream_id) .seqnum(state.seqnum) .group_id(state.group_id) .stream(stream.clone()) .build(); drop(state_guard); } _ => (), } gst::Pad::event_default(pad, Some(&*self.obj()), event) } fn proxy_pad_chain( &self, pad: &gst::ProxyPad, buffer: gst::Buffer, ) -> Result { let res = gst::ProxyPad::chain_default(pad, Some(&*self.obj()), buffer); let mut state_guard = self.state.lock(); let state = match &mut *state_guard { None => return res, Some(state) => state, }; state.flow_combiner.update_pad_flow(pad, res) } fn ghost_pad_event(&self, pad: &gst::GhostPad, event: gst::Event) -> bool { match event.view() { gst::EventView::Seek(_s_ev) => { let mut state_guard = self.state.lock(); let state = match &mut *state_guard { None => return false, Some(state) => state, }; state.seqnum = event.seqnum(); drop(state_guard); gst::Pad::event_default(pad, Some(&*self.obj()), event) } gst::EventView::SelectStreams(ss_ev) => { gst::debug!(CAT, imp = self, "Handling stream selection event"); self.handle_select_stream_event(ss_ev); true } _ => gst::Pad::event_default(pad, Some(&*self.obj()), event), } } fn create_image_converts( &self, filter_caps: &gst::Caps, fallback_source: bool, ) -> gst::Element { let imagefreeze = gst::ElementFactory::make("imagefreeze") .property("is-live", true) .build() .expect("No imagefreeze found"); if !fallback_source || filter_caps.is_any() { return imagefreeze; } let bin = gst::Bin::default(); let videoconvert = gst::ElementFactory::make("videoconvert") .name("video_videoconvert") .build() .expect("No videoconvert found"); let videoscale = gst::ElementFactory::make("videoscale") .name("video_videoscale") .build() .expect("No videoscale found"); let capsfilter = gst::ElementFactory::make("capsfilter") .name("video_capsfilter") .property("caps", filter_caps) .build() .expect("No capsfilter found"); bin.add_many([&videoconvert, &videoscale, &imagefreeze, &capsfilter]) .unwrap(); gst::Element::link_many([&videoconvert, &videoscale, &imagefreeze, &capsfilter]).unwrap(); let ghostpad = gst::GhostPad::with_target(&videoconvert.static_pad("sink").unwrap()).unwrap(); ghostpad.set_active(true).unwrap(); bin.add_pad(&ghostpad).unwrap(); let ghostpad = gst::GhostPad::with_target(&capsfilter.static_pad("src").unwrap()).unwrap(); ghostpad.set_active(true).unwrap(); bin.add_pad(&ghostpad).unwrap(); bin.upcast() } fn create_video_converts( &self, filter_caps: &gst::Caps, fallback_source: bool, ) -> gst::Element { if !fallback_source || filter_caps.is_any() { return gst::ElementFactory::make("identity") .build() .expect("No identity found"); } let bin = gst::Bin::default(); let videoconvert = gst::ElementFactory::make("videoconvert") .name("video_videoconvert") .build() .expect("No videoconvert found"); let videoscale = gst::ElementFactory::make("videoscale") .name("video_videoscale") .build() .expect("No videoscale found"); let capsfilter = gst::ElementFactory::make("capsfilter") .name("video_capsfilter") .property("caps", filter_caps) .build() .expect("No capsfilter found"); bin.add_many([&videoconvert, &videoscale, &capsfilter]) .unwrap(); gst::Element::link_many([&videoconvert, &videoscale, &capsfilter]).unwrap(); let ghostpad = gst::GhostPad::with_target(&videoconvert.static_pad("sink").unwrap()).unwrap(); ghostpad.set_active(true).unwrap(); bin.add_pad(&ghostpad).unwrap(); let ghostpad = gst::GhostPad::with_target(&capsfilter.static_pad("src").unwrap()).unwrap(); ghostpad.set_active(true).unwrap(); bin.add_pad(&ghostpad).unwrap(); bin.upcast() } fn create_audio_converts( &self, filter_caps: &gst::Caps, fallback_source: bool, ) -> gst::Element { if !fallback_source || filter_caps.is_any() { return gst::ElementFactory::make("identity") .build() .expect("No identity found"); } let bin = gst::Bin::default(); let audioconvert = gst::ElementFactory::make("audioconvert") .name("audio_audioconvert") .build() .expect("No audioconvert found"); let audioresample = gst::ElementFactory::make("audioresample") .name("audio_audioresample") .build() .expect("No audioresample found"); let capsfilter = gst::ElementFactory::make("capsfilter") .name("audio_capsfilter") .property("caps", filter_caps) .build() .expect("No capsfilter found"); bin.add_many([&audioconvert, &audioresample, &capsfilter]) .unwrap(); gst::Element::link_many([&audioconvert, &audioresample, &capsfilter]).unwrap(); let ghostpad = gst::GhostPad::with_target(&audioconvert.static_pad("sink").unwrap()).unwrap(); ghostpad.set_active(true).unwrap(); bin.add_pad(&ghostpad).unwrap(); let ghostpad = gst::GhostPad::with_target(&capsfilter.static_pad("src").unwrap()).unwrap(); ghostpad.set_active(true).unwrap(); bin.add_pad(&ghostpad).unwrap(); bin.upcast() } fn handle_source_pad_added( &self, pad: &gst::Pad, fallback_source: bool, ) -> Result<(), gst::ErrorMessage> { gst::debug!( CAT, imp = self, "Pad {} added to {}source", pad.name(), if fallback_source { "fallback " } else { "" } ); let mut state_guard = self.state.lock(); let state = match &mut *state_guard { None => { return Ok(()); } Some(state) => state, }; self.setup_output_branch(state, pad, fallback_source)?; drop(state_guard); self.obj().notify("status"); Ok(()) } /// Sets up an OutputBranch for a matching Output, which it finds based on the stream ID. /// Used by pad-added callback, as well as during relinking if main<->fallback stream mapping changes. fn setup_output_branch( &self, state: &mut State, pad: &gst::Pad, is_fallback: bool, ) -> Result<(), gst::ErrorMessage> { let mut is_image = false; if let Some(ev) = pad.sticky_event::(0) { let stream = ev.stream(); if let Some(caps) = stream.and_then(|s| s.caps()) { if let Some(s) = caps.structure(0) { is_image = s.name().starts_with("image/"); } } } let source = if is_fallback { if let Some(ref mut source) = state.fallback_source { if source.posted_streams.is_none() { gst::error!( CAT, imp = self, "Got fallback source pads before stream collection" ); return Err(gst::error_msg!( gst::CoreError::StateChange, ["Got fallback source pads before stream collection"] )); } source } else { return Ok(()); } } else { if state.source.posted_streams.is_none() { gst::error!( CAT, imp = self, "Got main source pads before stream collection" ); return Err(gst::error_msg!( gst::CoreError::StateChange, ["Got main source pads before stream collection"] )); } &mut state.source }; if is_image { if let Some(timeout) = source.pending_restart_timeout.take() { timeout.unschedule(); } if let Some(timeout) = source.retry_timeout.take() { timeout.unschedule(); } if let Some(timeout) = source.restart_timeout.take() { timeout.unschedule(); } } source.is_image |= is_image; let stream_id = pad.stream_id().unwrap(); let (is_video, stream) = { let Some(stream) = state.streams.iter_mut().find(|s| { if is_fallback { s.fallback_id.as_ref() == Some(&stream_id) } else { s.main_id.as_ref() == Some(&stream_id) } }) else { gst::warning!( CAT, imp = self, "Source added an unwanted pad with stream id {stream_id}!" ); return Ok(()); }; let is_video = stream.gst_stream.stream_type() == gst::StreamType::VIDEO; (is_video, stream) }; let (branch_storage, filter_caps, switch) = match stream.output { None => { gst::warning!( CAT, imp = self, "Source added an unwanted pad with stream id {stream_id}!" ); return Ok(()); } Some(Output { ref mut main_branch, ref switch, ref filter_caps, .. }) if !is_fallback => { if main_branch.is_some() { gst::warning!( CAT, imp = self, "Already configured main stream for {}", stream_id ); return Ok(()); } (main_branch, filter_caps, switch) } Some(Output { ref mut fallback_branch, ref switch, ref filter_caps, .. }) => { if fallback_branch.is_some() { gst::warning!( CAT, imp = self, "Already configured a fallback stream for {}", stream_id ); return Ok(()); } (fallback_branch, filter_caps, switch) } }; // FIXME: This adds/removes pads while having the state locked // Configure conversion elements only for fallback stream // (if fallback caps is not ANY) or image source. let converters = if is_image { self.create_image_converts(filter_caps, is_fallback) } else if is_video { self.create_video_converts(filter_caps, is_fallback) } else { self.create_audio_converts(filter_caps, is_fallback) }; let queue = gst::ElementFactory::make("queue") .property("max-size-bytes", 0u32) .property("max-size-buffers", 0u32) .property("max-size-time", 1.seconds()) .build() .unwrap(); let clocksync = gst::ElementFactory::make("clocksync") .build() .unwrap_or_else(|_| { let identity = gst::ElementFactory::make("identity") .property("sync", true) .build() .unwrap(); identity }); source .bin .add_many([&converters, &queue, &clocksync]) .unwrap(); converters.sync_state_with_parent().unwrap(); queue.sync_state_with_parent().unwrap(); clocksync.sync_state_with_parent().unwrap(); let sinkpad = converters.static_pad("sink").unwrap(); pad.link(&sinkpad).map_err(|err| { gst::error!(CAT, imp = self, "Failed to link new source pad: {}", err); gst::error_msg!( gst::CoreError::Negotiation, ["Failed to link new source pad: {}", err] ) })?; gst::Element::link_many([&converters, &queue, &clocksync]).unwrap(); let ghostpad = gst::GhostPad::builder_with_target(&clocksync.static_pad("src").unwrap()) .unwrap() .name(pad.name()) .build(); let _ = ghostpad.set_active(true); source.bin.add_pad(&ghostpad).unwrap(); // Link the new source pad in let switch_pad = switch.request_pad_simple("sink_%u").unwrap(); switch_pad.set_property("priority", u32::from(is_fallback)); ghostpad .link_full( &switch_pad, gst::PadLinkCheck::all() & !gst::PadLinkCheck::CAPS, ) .unwrap(); let stream_id = stream.gst_stream.stream_id().unwrap(); let eos_probe = pad .add_probe(gst::PadProbeType::EVENT_DOWNSTREAM, move |pad, info| { let element = match pad .parent() .and_then(|p| p.parent()) .and_then(|p| p.parent()) .and_then(|p| p.downcast::().ok()) { None => return gst::PadProbeReturn::Ok, Some(element) => element, }; let imp = element.imp(); let Some(ev) = info.event() else { return gst::PadProbeReturn::Ok; }; if ev.type_() != gst::EventType::Eos { return gst::PadProbeReturn::Ok; } gst::debug!( CAT, obj = element, "Received EOS from {}source on pad {}", if is_fallback { "fallback " } else { "" }, pad.name() ); let mut state_guard = imp.state.lock(); let state = match &mut *state_guard { None => { return gst::PadProbeReturn::Ok; } Some(state) => state, }; if is_image { gst::PadProbeReturn::Ok } else if state.settings.restart_on_eos || is_fallback { imp.handle_source_error(state, RetryReason::Eos, is_fallback); drop(state_guard); element.notify("statistics"); gst::PadProbeReturn::Drop } else { // EOS all streams if all main branches are EOS let mut sinkpads = vec![]; if let Some(output) = { state .streams .iter() .find(|s| s.gst_stream.stream_id().unwrap() == stream_id) .and_then(|s| s.output.as_ref()) } { sinkpads.extend(output.switch.sink_pads().into_iter().filter(|p| p != pad)); }; let all_main_branches_eos = state .streams .iter() .filter_map(|s| s.output.as_ref()) .all(|o| { o.main_branch .as_ref() // no main branch = ignore .is_none_or(|b| { &b.queue_srcpad == pad // now check if the pad is EOS && b.queue_srcpad.pad_flags().contains(gst::PadFlags::EOS) }) }); if all_main_branches_eos { // EOS all fallbackswitches of outputs without main branch for output in state .streams .iter() .filter_map(|s| s.output.as_ref()) .filter(|o| o.main_branch.is_none()) { sinkpads .extend(output.switch.sink_pads().into_iter().filter(|p| p != pad)); } } let event = ev.clone(); element.call_async(move |_| { for sinkpad in sinkpads { sinkpad.send_event(event.clone()); } }); gst::PadProbeReturn::Ok } }) .unwrap(); let queue_srcpad = queue.static_pad("src").unwrap(); let source_srcpad_block = Some(self.add_pad_probe(pad, &queue_srcpad, is_fallback)); *branch_storage = Some(OutputBranch { source_srcpad: pad.clone(), source_srcpad_block, eos_probe: Some(eos_probe), clocksync, converters, queue, queue_srcpad, switch_pad, ghostpad, }); Ok(()) } fn add_pad_probe(&self, pad: &gst::Pad, block_pad: &gst::Pad, fallback_source: bool) -> Block { // FIXME: Not literally correct as we add the probe to the queue source pad but that's only // a workaround until // https://gitlab.freedesktop.org/gstreamer/gst-plugins-base/-/issues/800 // is fixed. gst::debug!( CAT, imp = self, "Adding blocking probe to pad {} for pad {} (fallback: {})", block_pad.name(), pad.name(), fallback_source, ); let probe_id = block_pad .add_probe( gst::PadProbeType::BLOCK | gst::PadProbeType::BUFFER | gst::PadProbeType::EVENT_DOWNSTREAM, move |inner_pad, info| { let element = match inner_pad .parent() .and_then(|p| p.parent()) .and_then(|p| p.parent()) .and_then(|p| p.downcast::().ok()) { None => return gst::PadProbeReturn::Ok, Some(element) => element, }; let pts = match info.data { Some(gst::PadProbeData::Buffer(ref buffer)) => buffer.pts(), Some(gst::PadProbeData::Event(ref ev)) => match ev.view() { gst::EventView::Gap(ev) => Some(ev.get().0), _ => return gst::PadProbeReturn::Pass, }, _ => unreachable!(), }; let imp = element.imp(); if let Err(msg) = imp.handle_pad_blocked(inner_pad, pts, fallback_source) { imp.post_error_message(msg); } gst::PadProbeReturn::Ok }, ) .unwrap(); let qos_probe_id = block_pad .add_probe(gst::PadProbeType::EVENT_UPSTREAM, |_pad, info| { let Some(ev) = info.event() else { return gst::PadProbeReturn::Ok; }; if ev.type_() != gst::EventType::Qos { return gst::PadProbeReturn::Ok; } gst::PadProbeReturn::Drop }) .unwrap(); Block { pad: block_pad.clone(), probe_id, qos_probe_id, running_time: gst::ClockTime::NONE, } } fn handle_pad_blocked( &self, pad: &gst::Pad, pts: impl Into>, fallback_source: bool, ) -> Result<(), gst::ErrorMessage> { let mut state_guard = self.state.lock(); let state = match &mut *state_guard { None => { return Ok(()); } Some(state) => state, }; let (branch, source) = { let output = state .streams .iter_mut() .filter_map(|s| s.output.as_mut()) .find(|o| { if fallback_source { o.fallback_branch .as_ref() .is_some_and(|b| &b.queue_srcpad == pad) } else { o.main_branch .as_ref() .is_some_and(|b| &b.queue_srcpad == pad) } }) .unwrap(); let branch = if fallback_source { output.fallback_branch.as_mut().unwrap() } else { output.main_branch.as_mut().unwrap() }; let source = if fallback_source { if let Some(ref source) = state.fallback_source { source } else { return Ok(()); } } else { &state.source }; (branch, source) }; gst::debug!( CAT, imp = self, "Called probe on pad {} for pad {} (fallback: {})", pad.name(), branch.source_srcpad.name(), fallback_source ); // Directly unblock for live streams if source.is_live { if let Some(block) = branch.source_srcpad_block.take() { gst::debug!( CAT, imp = self, "Removing pad probe on pad {} for pad {} (fallback: {})", pad.name(), branch.source_srcpad.name(), fallback_source, ); block.pad.remove_probe(block.probe_id); block.pad.remove_probe(block.qos_probe_id); } gst::debug!(CAT, imp = self, "Live source, unblocking directly"); drop(state_guard); self.obj().notify("status"); return Ok(()); } // Update running time for this block let block = match branch.source_srcpad_block { Some(ref mut block) => block, None => return Ok(()), }; let segment = match pad.sticky_event::(0) { Some(ev) => ev.segment().clone(), None => { gst::warning!(CAT, imp = self, "Have no segment event yet"); return Ok(()); } }; let segment = segment.downcast::().map_err(|_| { gst::error!(CAT, imp = self, "Have no time segment"); gst::error_msg!(gst::CoreError::Clock, ["Have no time segment"]) })?; let pts = pts.into(); let running_time = if let Some((_, start)) = pts.zip(segment.start()).filter(|(pts, start)| pts < start) { segment.to_running_time(start) } else if let Some((_, stop)) = pts.zip(segment.stop()).filter(|(pts, stop)| pts >= stop) { segment.to_running_time(stop) } else { segment.to_running_time(pts) }; gst::debug!( CAT, imp = self, "Have block running time {}", running_time.display(), ); block.running_time = running_time; self.unblock_pads(state, fallback_source); drop(state_guard); self.obj().notify("status"); Ok(()) } fn unblock_pads(&self, state: &mut State, fallback_source: bool) { let current_running_time = match self.obj().current_running_time() { Some(current_running_time) => current_running_time, None => { gst::debug!(CAT, imp = self, "Waiting for current_running_time"); return; } }; if !fallback_source && state.manually_blocked { gst::debug!(CAT, imp = self, "Not unblocking yet: manual unblock"); return; } // Check if all streams are blocked and have a running time and we have // 100% buffering if (fallback_source && state.stats.fallback_buffering_percent < 100) || (!fallback_source && state.stats.buffering_percent < 100) { gst::debug!( CAT, imp = self, "Not unblocking yet: buffering {}%", state.stats.buffering_percent ); return; } let source = if fallback_source { if let Some(ref source) = state.fallback_source { source } else { // There are no blocked pads if there is no fallback source return; } } else { &state.source }; if source.posted_streams.is_none() { gst::debug!(CAT, imp = self, "Have no stream collection yet"); return; }; let mut branches = state .streams .iter_mut() .filter_map(|s| s.output.as_mut()) .filter_map(|o| { if fallback_source { o.fallback_branch.as_mut() } else { o.main_branch.as_mut() } }) .collect::>(); let mut min_running_time = gst::ClockTime::NONE; for branch in branches.iter() { let running_time = branch .source_srcpad_block .as_ref() .and_then(|b| b.running_time); let srcpad = branch.source_srcpad.clone(); let is_eos = srcpad.pad_flags().contains(gst::PadFlags::EOS); if running_time.is_none() && !is_eos { gst::debug!( CAT, imp = self, "Waiting for all pads to block (fallback: {})", fallback_source ); return; } if is_eos { gst::debug!( CAT, imp = self, "Ignoring EOS pad for running time (fallback: {})", fallback_source ); // not checking running time for EOS pads continue; } let running_time = running_time.unwrap(); if min_running_time.is_none_or(|min_running_time| running_time < min_running_time) { min_running_time = Some(running_time); } } let offset = min_running_time.map(|min_running_time| { if current_running_time > min_running_time { (current_running_time - min_running_time).nseconds() as i64 } else { -((min_running_time - current_running_time).nseconds() as i64) } }); gst::debug!( CAT, imp = self, "Unblocking at {} with pad offset {:?}, is_fallback: {}", current_running_time, offset, fallback_source ); for branch in branches.iter_mut() { if let Some(block) = branch.source_srcpad_block.take() { let srcpad = branch.source_srcpad.clone(); let is_eos = srcpad.pad_flags().contains(gst::PadFlags::EOS); if !is_eos { if let Some(offset) = offset { block.pad.set_offset(offset); } } block.pad.remove_probe(block.probe_id); block.pad.remove_probe(block.qos_probe_id); } } } /// Remove OutputBranch from the Output if there is still one. fn handle_source_pad_removed(&self, pad: &gst::Pad, fallback_source: bool) { gst::debug!( CAT, imp = self, "Pad {} removed from {}source", pad.name(), if fallback_source { "fallback " } else { "" } ); let mut state_guard = self.state.lock(); let state = match &mut *state_guard { None => { return; } Some(state) => state, }; let (mut branch, source, switch) = { let Some(stream) = state.streams.iter_mut().find(|s| { let Some(ref o) = s.output else { return false }; if fallback_source { o.fallback_branch .as_ref() .is_some_and(|b| &b.source_srcpad == pad) } else { o.main_branch .as_ref() .is_some_and(|b| &b.source_srcpad == pad) } }) else { return; }; let output = stream.output.as_mut().unwrap(); let branch: OutputBranch = if fallback_source { output.fallback_branch.take().unwrap() } else { output.main_branch.take().unwrap() }; let source = if fallback_source { if let Some(ref source) = state.fallback_source { source } else { return; } } else { &state.source }; (branch, source.bin.clone(), output.switch.clone()) }; drop(state_guard); self.handle_branch_teardown(&switch, &source, &mut branch, fallback_source); state_guard = self.state.lock(); let state = match &mut *state_guard { None => { return; } Some(state) => state, }; self.unblock_pads(state, fallback_source); drop(state_guard); self.obj().notify("status"); } fn handle_branch_teardown( &self, switch: &gst::Element, source: &gst::Bin, branch: &mut OutputBranch, is_fallback: bool, ) { gst::debug!( CAT, imp = self, "Tearing down branch for pad {}, fallback: {}", branch.source_srcpad.name(), is_fallback ); branch.converters.set_locked_state(true); let _ = branch.converters.set_state(gst::State::Null); source.remove(&branch.converters).unwrap(); branch.queue.set_locked_state(true); let _ = branch.queue.set_state(gst::State::Null); source.remove(&branch.queue).unwrap(); branch.clocksync.set_locked_state(true); let _ = branch.clocksync.set_state(gst::State::Null); source.remove(&branch.clocksync).unwrap(); if branch.switch_pad.parent().as_ref() == Some(switch.upcast_ref()) { switch.release_request_pad(&branch.switch_pad); } let _ = branch.ghostpad.set_active(false); source.remove_pad(&branch.ghostpad).unwrap(); if let Some(block) = branch.source_srcpad_block.take() { block.pad.remove_probe(block.probe_id); block.pad.remove_probe(block.qos_probe_id); } } fn handle_buffering(&self, m: &gst::message::Buffering) { let mut state_guard = self.state.lock(); let state = match &mut *state_guard { None => { return; } Some(state) => state, }; let src = match m.src() { Some(src) => src, None => return, }; let fallback_source = if let Some(ref source) = state.fallback_source { src.has_as_ancestor(&source.bin) } else if src.has_as_ancestor(&state.source.bin) { false } else { return; }; let source = if fallback_source { if let Some(ref mut source) = state.fallback_source { source } else { return; } } else { &mut state.source }; if source.pending_restart { gst::debug!(CAT, imp = self, "Has pending restart"); return; } else if !source.running { gst::debug!(CAT, imp = self, "Was shut down"); return; } gst::log!( CAT, imp = self, "Got buffering {}% (fallback: {})", m.percent(), fallback_source ); let buffering_percent = if fallback_source { &mut state.stats.fallback_buffering_percent } else { &mut state.stats.buffering_percent }; let last_buffering_update = if fallback_source { &mut state.fallback_last_buffering_update } else { &mut state.last_buffering_update }; *buffering_percent = m.percent(); if *buffering_percent < 100 { *last_buffering_update = Some(Instant::now()); // Block source pads if needed to pause for output in state.streams.iter_mut().filter_map(|s| s.output.as_mut()) { let branch = match output { Output { main_branch: Some(ref mut branch), .. } if !fallback_source => branch, Output { fallback_branch: Some(ref mut branch), .. } if fallback_source => branch, _ => continue, }; if branch.source_srcpad_block.is_none() { branch.source_srcpad_block = Some(self.add_pad_probe( &branch.source_srcpad, &branch.queue_srcpad, fallback_source, )); } } } else { // Check if we can unblock now self.unblock_pads(state, fallback_source); } drop(state_guard); self.obj().notify("status"); self.obj().notify("statistics"); } /// Handles a stream collection from main/fallback sources. /// Calls try_bind_streams() which does the magic to bind stream IDs from the received collection /// with our actual output Streams. /// If that returns that it has changed the collection (= added or removed a stream on our side), /// we'll post a new stream collection. /// /// Also, even if we don't change the collection, try_bind_streams() might've bound /// one of the new source streams to one of our selected ones, so we call perform_selection() /// to send select-streams to the relevant source to give us that stream, BUT ignore the message /// it creates, as we're not changing our outputs in that case, just input :)) fn handle_stream_collection(&self, m: &gst::message::StreamCollection) { let mut state_guard = self.state.lock(); let mut state = match &mut *state_guard { None => { return; } Some(state) => state, }; let src = match m.src() { Some(src) => src, None => return, }; let is_fallback = if let Some(ref source) = state.fallback_source { src.has_as_ancestor(&source.bin) } else if src.has_as_ancestor(&state.source.bin) { false } else { return; }; let collection = m.stream_collection(); gst::debug!( CAT, imp = self, "Got stream collection {:?} (fallback: {})", collection.debug(), is_fallback, ); if is_fallback { if let Some(ref mut source) = state.fallback_source { source.posted_streams = Some(collection.clone()); } } else { state.source.posted_streams = Some(collection.clone()); } // We're guaranteed to not remove any streams that are selected or persistent // So we don't need to touch outputs here, only in stream-select! let should_post = self.try_bind_streams(state, &collection, is_fallback); // Need to check if any already present fallback streams have been remapped and relink them accordingly. // We can skip relinking if we're sure nothing was linked before, which is only if this is the first // fallback stream collection we got since startup. let (mut branches_relink, branches_correct) = { gst::debug!( CAT, imp = self, "Relinking fallback streams according to mapping" ); // If stream has no fallback bound (e.g. it disappeared) we do nothing, its branch will be removed in pad-removed. // If output has no fallback branch (e.g. main source had more streams than fallback), same case, handled in pad-added. // In all other cases we need to check if any fallback streams need to be rewired. let (mut relink, mut correct) = (vec![], vec![]); for stream in state.streams.iter_mut().filter(|s| { s.fallback_id.is_some() && s.output .as_ref() .is_some_and(|o| o.fallback_branch.is_some()) }) { let output = stream.output.as_mut().unwrap(); let branch = output.fallback_branch.as_ref().unwrap(); let gst_stream = branch.source_srcpad.stream().unwrap(); if gst_stream.stream_id() == stream.fallback_id { correct.push(output.fallback_branch.as_mut().unwrap()); } else { let switch = output.switch.clone(); let source = state.fallback_source.as_ref().unwrap().bin.clone(); let branch = output.fallback_branch.take().unwrap(); relink.push((switch, source, branch, None::, gst_stream)); } } (relink, correct) }; if !branches_relink.is_empty() { for branch in branches_correct { if branch.source_srcpad_block.is_none() { branch.source_srcpad_block = Some(self.add_pad_probe(&branch.source_srcpad, &branch.queue_srcpad, true)); } } drop(state_guard); for (switch, source, branch, probe, _) in branches_relink.iter_mut() { let srcpad = branch.source_srcpad.clone(); let stream_id = srcpad.stream_id().unwrap(); let cvar_pair = Arc::new((Mutex::new(false), Condvar::new())); let cvar_pair_clone = cvar_pair.clone(); gst::debug!( CAT, imp = self, "Removing fallback branch for pad {}, stream {}", srcpad.name(), stream_id ); *probe = Some( srcpad // Waiting for IDLE means there's no stream-id here afterwards for some reason // Workaround is below (manually restoring the stream-start event) .add_probe(gst::PadProbeType::IDLE, move |_, _| { let (lock, cvar) = &*cvar_pair_clone; *lock.lock() = true; cvar.notify_one(); gst::PadProbeReturn::Ok }) .unwrap(), ); let (lock, cvar) = &*cvar_pair; let mut blocked = lock.lock(); if !*blocked { cvar.wait(&mut blocked); } if let Some(eos_probe) = branch.eos_probe.take() { srcpad.remove_probe(eos_probe); } let peer_pad = srcpad.peer().unwrap(); srcpad.unlink(&peer_pad).unwrap(); self.handle_branch_teardown(switch, source, branch, true); } state_guard = self.state.lock(); state = match &mut *state_guard { None => { return; } Some(state) => state, }; for (_, _, branch, probe_id, stream) in branches_relink { let srcpad = branch.source_srcpad.clone(); gst::debug!( CAT, imp = self, "Setting up new fallback branch for pad {}, stream {}", srcpad.name(), srcpad.stream_id().unwrap() ); // Workaround for stream-id disappearing from the srcpad if we wait for IDLE in the probe let stream_start_event = gst::event::StreamStart::builder(&stream.stream_id().unwrap()) .seqnum(state.seqnum) .group_id(state.group_id) .stream(stream.clone()) .build(); let _ = srcpad.store_sticky_event(&stream_start_event); self.setup_output_branch(state, &srcpad, true).unwrap(); srcpad.remove_probe(probe_id.unwrap()); } self.unblock_pads(state, true); gst::debug!(CAT, imp = self, "Relinking done"); } if should_post { let collection = state.stream_collection(); let our_seqnum = gst::Seqnum::next(); state.selection_seqnum = our_seqnum; drop(state_guard); gst::debug!( CAT, imp = self, "Posting new stream collection {:?}", collection.debug(), ); let _ = self .obj() .post_message(gst::message::StreamCollection::builder(&collection).build()); state_guard = self.state.lock(); state = match &mut *state_guard { None => { return; } Some(state) => state, }; if state.selection_seqnum != our_seqnum { // application selected streams so perform_selection() was already called // no action needed, else see below return; } } // We didn't post a new collection, OR we did and application didn't select anything, // but we need to re-try sending select-streams because we could have bound new streams let selected_ids = state .selected_streams() .map(|s| s.gst_stream.stream_id().unwrap()) .collect::>(); // This call sends select-streams to the sources as necessary. // Since try_bind_streams() guarantees that any currently selected streams // will stay selected and won't be removed, we don't need the streams-selected message here. drop(state_guard); if let Some((_msg, events)) = self.perform_selection(&selected_ids) { for (element, event) in events { if !element.send_event(event) { gst::warning!( CAT, imp = self, "Sending select-streams to {} failed, streams might be missing", element.name() ); } } } } /// Attempts to match streams from the incoming collection /// with the streams (streams as in streamcollection, not outputs!) we have internally. /// Will also create new / remove old streams as needed. /// Guarantees that any currently selected streams will not be removed, just unbound and switched to fallback if needed. fn try_bind_streams( &self, state: &mut State, collection: &gst::StreamCollection, is_fallback: bool, ) -> bool { let mut collection_changed = false; // If a fallback stream disappears, just unbind it. // If a main stream disappears, remove the matching stream on our side, // unless it's marked as persistent or selected, in which case also unbind it. state.streams.retain_mut(|stream| { let id = match if is_fallback { stream.fallback_id.as_ref() } else { stream.main_id.as_ref() } { Some(id) => id, None => return true, }; if !collection .iter() .any(|s| s.stream_id().as_ref() == Some(id)) { if is_fallback { gst::debug!( CAT, imp = self, "Unbinding fallback stream {:?}", stream.gst_stream ); stream.fallback_id = None; } else if stream.output.is_some() || stream.persistent { // Persistent == one of the 'base' two streams // If persistent or selected, the output is guaranteed to stay (and fallback to dummy if needed), // so we don't have to ever send streams-selected if a source stream disappears. gst::debug!( CAT, imp = self, "Unbinding main stream from selected or persistent output {:?}", stream.gst_stream ); stream.main_id = None; } else { gst::debug!( CAT, imp = self, "Removing unused stream {:?}", stream.gst_stream ); collection_changed = true; return false; } } true }); if !is_fallback { // If it's from the main source, try to bind to existing streams or create new ones collection_changed |= self.try_bind_main_streams(state, collection); // Then ask for mapping and bind fallback streams to the new ones if let Some(fallback_collection) = state .fallback_source .as_ref() .and_then(|s| s.posted_streams.clone()) { self.try_bind_fallback_streams(state, &fallback_collection); } } else { // Otherwise just ask which fallback should go where, even if we don't have anything from the main source yet self.try_bind_fallback_streams(state, collection); } collection_changed } fn try_bind_main_streams(&self, state: &mut State, collection: &gst::StreamCollection) -> bool { let mut collection_changed = false; for stream in collection { // TODO: Handle subtitles and other stream types if stream.stream_type() != gst::StreamType::AUDIO && stream.stream_type() != gst::StreamType::VIDEO { continue; } if state .streams .iter() .any(|s| s.main_id.as_ref() == Some(&stream.stream_id().unwrap())) { continue; } // Main streams are always automatically matched to the first available slot we have. // If there are no streams with free slots, we create a new one. // try_bind_streams() will then ask for mapping to bind fallback streams to those. if let Some(suitable_stream) = state .streams .iter_mut() .find(|s| s.main_id.is_none() && s.gst_stream.stream_type() == stream.stream_type()) { suitable_stream.main_id = Some(stream.stream_id().unwrap()); gst::debug!( CAT, imp = self, "Bound main stream {:?} to our stream {:?}", stream.stream_id().unwrap(), suitable_stream.gst_stream.stream_id().unwrap() ); } else { let mut new_stream = self.create_stream(state, stream.stream_type(), false); new_stream.main_id = Some(stream.stream_id().unwrap()); gst::debug!( CAT, imp = self, "Adding stream for main {:?} mapped to our stream {:?}", stream.stream_id().unwrap(), new_stream.gst_stream.stream_id().unwrap() ); collection_changed = true; state.streams.push(new_stream); } } collection_changed } fn try_bind_fallback_streams(&self, state: &mut State, collection: &gst::StreamCollection) { let mut map = state.stream_map(); let user_map = self.obj().emit_by_name::>( "map-streams", &[ &map.clone(), &state.source.posted_streams.as_ref(), &collection, ], ); if let Some(new_map) = user_map { if new_map == map { gst::debug!(CAT, imp = self, "Stream map unchanged"); } else if self.verify_map_correct(&map, &new_map, collection) { gst::debug!( CAT, imp = self, "Stream map changed, binding fallback streams" ); map = new_map; } } let mut used_ids = vec![]; for our_stream in state.streams.iter_mut() { let id = our_stream.gst_stream.stream_id().unwrap(); let map_entry = map.get::<&gst::StructureRef>(&id).unwrap(); // Let's first remove all bindings on our side. // We'll bind according to the map first and then auto-match the rest. our_stream.fallback_id = None; if let Ok(fallback_id) = map_entry.get::("fallback") { let fallback_stream = collection .iter() .find(|s| s.stream_id().as_ref() == Some(&fallback_id)) .unwrap(); if fallback_stream.stream_type() != our_stream.gst_stream.stream_type() { gst::warning!( CAT, imp = self, "Fallback stream {:?} type mismatch: expected {:?}, got {:?}, ignoring mapping", fallback_stream.stream_id().unwrap(), our_stream.gst_stream.stream_type(), fallback_stream.stream_type() ); continue; } gst::debug!( CAT, imp = self, "Bound fallback stream {:?} to our stream {:?}", fallback_stream.stream_id().unwrap(), our_stream.gst_stream.stream_id().unwrap() ); our_stream.fallback_id = Some(fallback_id.clone()); used_ids.push(fallback_id); } } // At this point explicitly mapped streams have been bound, // and the rest can just be matched to first available slots. for fallback_stream in collection .iter() .filter(|s| !used_ids.contains(&s.stream_id().unwrap())) { // TODO: Handle subtitles and other stream types if fallback_stream.stream_type() != gst::StreamType::AUDIO && fallback_stream.stream_type() != gst::StreamType::VIDEO { continue; } if let Some(suitable_stream) = state.streams.iter_mut().find(|s| { s.fallback_id.is_none() && s.gst_stream.stream_type() == fallback_stream.stream_type() }) { suitable_stream.fallback_id = Some(fallback_stream.stream_id().unwrap()); gst::debug!( CAT, imp = self, "Bound fallback stream {:?} to our stream {:?}", fallback_stream.stream_id().unwrap(), suitable_stream.gst_stream.stream_id().unwrap() ); } } } /// Checks whether, in the provided map: /// - All entries for 'our' streams are present /// - None of the main stream mappings have changed /// - Chosen fallback IDs actually exist in the provided collection fn verify_map_correct( &self, our_map: &gst::Structure, user_map: &gst::Structure, fallback_collection: &gst::StreamCollection, ) -> bool { if our_map.len() != user_map.len() { gst::warning!( CAT, imp = self, "Amount of entries in map changed: {} -> {}, ignoring map", our_map.len(), user_map.len() ); return false; } for (id, entry) in our_map .iter() .map(|(id, entry)| (id, entry.get::<&gst::StructureRef>().unwrap())) { let user_entry = match user_map.get::<&gst::StructureRef>(id) { Ok(user_entry) => user_entry, Err(_) => { gst::warning!( CAT, imp = self, "Entry for stream ID {:?} missing, ignoring map", id ); return false; } }; match (entry.get::<&str>("main"), user_entry.get::<&str>("main")) { (Ok(main_id), Ok(user_main_id)) if main_id != user_main_id => { gst::warning!( CAT, imp = self, "Main stream mapping for stream {:?} changed: {:?} -> {:?}, ignoring map", id, main_id, user_main_id ); return false; } (Ok(_), Err(_)) => { gst::warning!( CAT, imp = self, "Main stream mapping for stream {:?} missing, ignoring map", id ); return false; } _ => {} } if let Ok(fallback_id) = entry.get::<&str>("fallback") { if !fallback_collection .iter() .any(|s| s.stream_id().as_ref().map(|id| id.as_str()) == Some(fallback_id)) { gst::warning!( CAT, imp = self, "Fallback stream ID {:?} not from fallback collection, ignoring map", fallback_id ); return false; } } } true } /// Simply calls perform_selection() to give the user outputs for the streams they've selected fn handle_select_stream_event(&self, e: &gst::event::SelectStreams) -> bool { let mut state_guard = self.state.lock(); let state = match &mut *state_guard { None => { return false; } Some(state) => state, }; let seqnum = e.seqnum(); if state.selection_seqnum == seqnum { gst::debug!( CAT, imp = self, "select-streams with seqnum {:?} already handled", seqnum ); return true; } let selected_streams = e .streams() .into_iter() .map(glib::GString::from) .collect::>(); gst::debug!( CAT, imp = self, "Got select-streams event with streams {:?}", selected_streams ); drop(state_guard); if let Some((msg, events)) = self.perform_selection(&selected_streams) { self.state.lock().as_mut().unwrap().selection_seqnum = seqnum; for (element, event) in events { if !element.send_event(event) { gst::error!( CAT, imp = self, "Sending select-streams to {} failed", element.name() ); return false; } } gst::debug!(CAT, imp = self, "Posting streams-selected message"); let _ = self.obj().post_message(msg); return true; } // perform_selection fail indicates an incorrect stream being specified false } /// Goes through all streams we have, checks if they're on the selected list. /// /// Checks which of them don't have an output yet and creates one if needed. /// If any streams have outputs but are not selected anymore, they're removed. /// /// Afterwards, we send select-streams to main/fallback sources to give us the matching streams. /// At the end, this returns a StreamsSelected message that can be posted to the pipeline by /// the caller if needed, and the SelectStreams events that are to be sent to the source(s). /// /// Needs state to be unlocked before calling. fn perform_selection( &self, selected_stream_ids: &[glib::GString], ) -> Option<(gst::Message, Vec<(gst::Element, gst::Event)>)> { let mut state_guard = self.state.lock(); let state = match &mut *state_guard { None => { return None; } Some(state) => state, }; let stream_ids = state .streams .iter() .map(|s| s.gst_stream.stream_id().unwrap()) .collect::>(); gst::debug!( CAT, imp = self, "Stream IDs: {:?}, select stream IDs: {:?}", stream_ids, selected_stream_ids ); if selected_stream_ids .iter() .any(|id| !stream_ids.iter().any(|other_id| id == other_id)) { gst::warning!( CAT, imp = self, "Got unknown stream in select-streams event" ); return None; } // Removing outputs is susceptible to deadlocking with state being locked, // so we do it separately after we've checked all streams. // Adding should ideally also be done separately, but so far hasn't caused issues. let mut outputs_to_remove = vec![]; for stream in &mut state.streams { let is_selected = selected_stream_ids .iter() .any(|id| id == &stream.gst_stream.stream_id().unwrap()); if stream.output.is_some() && !is_selected { gst::debug!( CAT, imp = self, "Scheduling output removal for stream {:?}", stream.gst_stream ); let output = stream.output.take().unwrap(); state.flow_combiner.remove_pad(&output.srcpad); outputs_to_remove.push(output); } else if stream.output.is_none() && is_selected { let is_audio = stream .gst_stream .stream_type() .contains(gst::StreamType::AUDIO); let fallback_caps = if is_audio { state.settings.fallback_audio_caps.clone() } else { state.settings.fallback_video_caps.clone() }; let number = if is_audio { state.num_audio_pads += 1; state.num_audio_pads - 1 } else { state.num_video_pads += 1; state.num_video_pads - 1 }; let output = self.create_output( &stream.gst_stream, state.settings.timeout, state.settings.min_latency, is_audio, number, state.settings.immediate_fallback, &fallback_caps, state.seqnum, state.group_id, ); state.flow_combiner.add_pad(&output.srcpad); stream.output = Some(output); gst::debug!( CAT, imp = self, "Added output for stream {:?}", stream.gst_stream.debug() ); } } drop(state_guard); for output in outputs_to_remove { gst::debug!( CAT, imp = self, "Removing output for stream {:?}", output.srcpad ); self.remove_output(output); } let mut state_guard = self.state.lock(); let state = match &mut *state_guard { None => { return None; } Some(state) => state, }; let mut events = Vec::new(); // send selected streams to main and fallback sources let main_ids = state .selected_streams() .filter_map(|s| s.main_id.as_ref()) .map(|s| s.as_str()) .collect::>(); if !main_ids.is_empty() { gst::debug!( CAT, imp = self, "Sending select-streams event to main source with streams {:?}", main_ids ); let main_event = gst::event::SelectStreams::builder(main_ids).build(); events.push((state.source.source.clone(), main_event)); } if let Some(ref source) = state.fallback_source { let fallback_ids = state .selected_streams() .filter_map(|s| s.fallback_id.as_ref()) .map(|s| s.as_str()) .collect::>(); if !fallback_ids.is_empty() { gst::debug!( CAT, imp = self, "Sending select-streams event to fallback source with streams {:?}", fallback_ids ); let fallback_event = gst::event::SelectStreams::builder(fallback_ids).build(); events.push((source.source.clone(), fallback_event)); } }; let selected_streams = state .streams .iter() .filter_map(|s| { if s.output.is_some() { Some(s.gst_stream.clone()) } else { None } }) .collect::>(); let collection = state.stream_collection(); // The collection passed here is 'all streams we have' // and then the streams are the selected ones - can be a bit confusing ;) Some(( gst::message::StreamsSelected::builder(&collection) .streams(&selected_streams) .build(), events, )) } fn handle_streams_selected(&self, m: &gst::message::StreamsSelected) { let mut state_guard = self.state.lock(); let state = match &mut *state_guard { None => { return; } Some(state) => state, }; let src = match m.src() { Some(src) => src, None => return, }; let fallback_source = if let Some(ref source) = state.fallback_source { src.has_as_ancestor(&source.bin) } else if src.has_as_ancestor(&state.source.bin) { false } else { return; }; let streams = m.streams(); gst::debug!( CAT, imp = self, "Got streams selected {:?} (fallback: {})", streams.map(|s| s.stream_id().unwrap()).collect::>(), fallback_source, ); // This might not be the first stream collection and we might have some unblocked pads from // before already, which would need to be blocked again now for keeping things in sync for branch in state .streams .iter_mut() .filter_map(|s| s.output.as_mut()) .filter_map(|o| { if fallback_source { o.fallback_branch.as_mut() } else { o.main_branch.as_mut() } }) { if branch.source_srcpad_block.is_none() { branch.source_srcpad_block = Some(self.add_pad_probe( &branch.source_srcpad, &branch.queue_srcpad, fallback_source, )); } } self.unblock_pads(state, fallback_source); drop(state_guard); self.obj().notify("status"); } fn handle_error(&self, m: &gst::message::Error) -> bool { let mut state_guard = self.state.lock(); let state = match &mut *state_guard { None => { return false; } Some(state) => state, }; let src = match m.src().and_then(|s| s.downcast_ref::()) { None => return false, Some(src) => src, }; gst::debug!( CAT, imp = self, "Got error message from {}: {}", src.path_string(), m.error() ); if src == &state.source.bin || src.has_as_ancestor(&state.source.bin) { self.handle_source_error(state, RetryReason::Error, false); drop(state_guard); self.obj().notify("status"); self.obj().notify("statistics"); return true; } // Check if error is from fallback input and if so, use a dummy fallback if let Some(ref source) = state.fallback_source { if src == &source.bin || src.has_as_ancestor(&source.bin) { self.handle_source_error(state, RetryReason::Error, true); drop(state_guard); self.obj().notify("status"); self.obj().notify("statistics"); return true; } } gst::error!( CAT, imp = self, "Give up for error message from {}", src.path_string() ); false } fn handle_source_error(&self, state: &mut State, reason: RetryReason, fallback_source: bool) { gst::debug!( CAT, imp = self, "Handling source error (fallback: {}): {:?}", fallback_source, reason ); if fallback_source { state.stats.last_fallback_retry_reason = reason; } else { state.stats.last_retry_reason = reason; } let source = if fallback_source { state.fallback_source.as_mut().unwrap() } else { &mut state.source }; if source.pending_restart { gst::debug!( CAT, imp = self, "{}source is already pending restart", if fallback_source { "fallback " } else { "" } ); return; } else if !source.running { gst::debug!( CAT, imp = self, "{}source was shut down", if fallback_source { "fallback " } else { "" } ); return; } // Increase retry count only if there was no pending restart if fallback_source { state.stats.num_fallback_retry += 1; } else { state.stats.num_retry += 1; } // Unschedule pending timeout, we're restarting now if let Some(timeout) = source.restart_timeout.take() { timeout.unschedule(); } // Prevent state changes from changing the state in an uncoordinated way source.pending_restart = true; // Drop any EOS events from any source pads of the source that might happen because of the // error. We don't need to remove these pad probes because restarting the source will also // remove/add the pads again. for pad in source.bin.src_pads() { pad.add_probe(gst::PadProbeType::EVENT_DOWNSTREAM, |_pad, info| { let Some(ev) = info.event() else { return gst::PadProbeReturn::Pass; }; if ev.type_() != gst::EventType::Eos { return gst::PadProbeReturn::Pass; } gst::PadProbeReturn::Drop }) .unwrap(); } let source_weak = source.bin.downgrade(); self.obj().call_async(move |element| { let imp = element.imp(); let Some(source) = source_weak.upgrade() else { return; }; // Remove blocking pad probes if they are still there as otherwise shutting down the // source will deadlock on the probes. let mut state_guard = imp.state.lock(); let state = match &mut *state_guard { None => { gst::debug!( CAT, imp = imp, "Restarting {}source not needed anymore", if fallback_source { "fallback " } else { "" } ); return; } Some(State { source, .. }) if !fallback_source && (!source.pending_restart || !source.running) => { gst::debug!(CAT, imp = imp, "Restarting source not needed anymore"); return; } Some(State { fallback_source: Some(ref source), .. }) if fallback_source && (!source.pending_restart || !source.running) => { gst::debug!( CAT, imp = imp, "Restarting fallback source not needed anymore", ); return; } Some(state) => state, }; for (source_srcpad, block) in state .streams .iter_mut() .filter_map(|s| s.output.as_mut()) .filter_map(|o| { if fallback_source { o.fallback_branch.as_mut() } else { o.main_branch.as_mut() } }) .filter_map(|branch| { if let Some(block) = branch.source_srcpad_block.take() { Some((&branch.source_srcpad, block)) } else { None } }) { gst::debug!( CAT, imp = imp, "Removing pad probe for pad {}", source_srcpad.name() ); block.pad.remove_probe(block.probe_id); block.pad.remove_probe(block.qos_probe_id); } let switch_sinkpads = state .streams .iter() .filter_map(|s| s.output.as_ref()) .filter_map(|o| { if fallback_source { o.fallback_branch.as_ref() } else { o.main_branch.as_ref() } }) .map(|branch| branch.switch_pad.clone()) .collect::>(); drop(state_guard); gst::debug!(CAT, imp = imp, "Flushing source"); for pad in switch_sinkpads { let _ = pad.push_event(gst::event::FlushStart::builder().build()); if let Some(switch) = pad.parent().map(|p| p.downcast::().unwrap()) { switch.release_request_pad(&pad); } } gst::debug!( CAT, imp = imp, "Shutting down {}source", if fallback_source { "fallback " } else { "" } ); let _ = source.set_state(gst::State::Null); // Sleep for 1s before retrying let mut state_guard = imp.state.lock(); let state = match &mut *state_guard { None => { gst::debug!( CAT, imp = imp, "Restarting {}source not needed anymore", if fallback_source { "fallback " } else { "" } ); return; } Some(State { source, .. }) if !fallback_source && (!source.pending_restart || !source.running) => { gst::debug!(CAT, imp = imp, "Restarting source not needed anymore"); return; } Some(State { fallback_source: Some(ref source), .. }) if fallback_source && (!source.pending_restart || !source.running) => { gst::debug!( CAT, imp = imp, "Restarting fallback source not needed anymore", ); return; } Some(state) => state, }; for branch in state .streams .iter_mut() .filter_map(|s| s.output.as_mut()) .filter_map(|o| { if fallback_source { o.fallback_branch.as_mut() } else { o.main_branch.as_mut() } }) { branch.source_srcpad_block = None; } gst::debug!(CAT, imp = imp, "Waiting for 1s before retrying"); let clock = gst::SystemClock::obtain(); let wait_time = clock.time() + gst::ClockTime::SECOND; if fallback_source { assert!(state .fallback_source .as_ref() .map(|s| s.pending_restart_timeout.is_none()) .unwrap_or(true)); } else { assert!(state.source.pending_restart_timeout.is_none()); } let timeout = clock.new_single_shot_id(wait_time); let element_weak = element.downgrade(); timeout .wait_async(move |_clock, _time, _id| { let Some(element) = element_weak.upgrade() else { return; }; gst::debug!(CAT, obj = element, "Woke up, retrying"); element.call_async(move |element| { let imp = element.imp(); let mut state_guard = imp.state.lock(); let state = match &mut *state_guard { None => { gst::debug!( CAT, imp = imp, "Restarting {}source not needed anymore", if fallback_source { "fallback " } else { "" } ); return; } Some(State { source, .. }) if !fallback_source && (!source.pending_restart || !source.running) => { gst::debug!(CAT, imp = imp, "Restarting source not needed anymore"); return; } Some(State { fallback_source: Some(ref source), .. }) if fallback_source && (!source.pending_restart || !source.running) => { gst::debug!( CAT, imp = imp, "Restarting fallback source not needed anymore", ); return; } Some(state) => state, }; let (source, old_source) = if !fallback_source { if let Source::Uri(..) = state.configured_source { // FIXME: Create a new uridecodebin3 because it currently is not reusable // See https://gitlab.freedesktop.org/gstreamer/gst-plugins-base/-/issues/746 element.remove(&state.source.bin).unwrap(); let mut source = imp.create_main_input( &state.configured_source, state.settings.buffer_duration, ); source.running = state.source.running; ( source.bin.clone(), Some(mem::replace(&mut state.source, source)), ) } else { state.source.pending_restart = false; state.source.pending_restart_timeout = None; state.stats.buffering_percent = 100; state.last_buffering_update = None; if let Some(timeout) = state.source.restart_timeout.take() { gst::debug!(CAT, imp = imp, "Unscheduling restart timeout"); timeout.unschedule(); } (state.source.bin.clone(), None) } } else if let Some(ref mut source) = state.fallback_source { source.pending_restart = false; source.pending_restart_timeout = None; state.stats.fallback_buffering_percent = 100; state.fallback_last_buffering_update = None; if let Some(timeout) = source.restart_timeout.take() { gst::debug!(CAT, imp = imp, "Unscheduling restart timeout"); timeout.unschedule(); } (source.bin.clone(), None) } else { return; }; drop(state_guard); if let Some(old_source) = old_source { // Drop old source after releasing the lock, it might call the pad-removed callback // still drop(old_source); } if source.sync_state_with_parent().is_err() { gst::error!( CAT, imp = imp, "{}source failed to change state", if fallback_source { "fallback " } else { "" } ); let _ = source.set_state(gst::State::Null); let mut state_guard = imp.state.lock(); let Some(ref mut state) = &mut *state_guard else { return; }; imp.handle_source_error( state, RetryReason::StateChangeFailure, fallback_source, ); drop(state_guard); element.notify("statistics"); } else { let mut state_guard = imp.state.lock(); let Some(ref mut state) = &mut *state_guard else { return; }; let source = if fallback_source { if let Some(source) = &state.fallback_source { source } else { return; } } else { &state.source }; if source.restart_timeout.is_none() { imp.schedule_source_restart_timeout( state, gst::ClockTime::ZERO, fallback_source, ); } } }); }) .expect("Failed to wait async"); if fallback_source { if let Some(ref mut source) = state.fallback_source { source.pending_restart_timeout = Some(timeout); } } else { state.source.pending_restart_timeout = Some(timeout); } }); } #[allow(clippy::blocks_in_conditions)] fn schedule_source_restart_timeout( &self, state: &mut State, elapsed: gst::ClockTime, fallback_source: bool, ) { if fallback_source { gst::fixme!( CAT, imp = self, "Restart timeout not implemented for fallback source" ); return; } let source = if fallback_source { if let Some(ref mut source) = state.fallback_source { source } else { return; } } else { &mut state.source }; if source.pending_restart { gst::debug!( CAT, imp = self, "Not scheduling {}source restart timeout because source is pending restart already", if fallback_source { "fallback " } else { "" }, ); return; } else if !source.running { gst::debug!( CAT, imp = self, "Not scheduling {}source restart timeout because source was shut down", if fallback_source { "fallback " } else { "" }, ); return; } if source.is_image { gst::debug!( CAT, imp = self, "Not scheduling {}source restart timeout because we are playing back an image", if fallback_source { "fallback " } else { "" }, ); return; } if !fallback_source && state.manually_blocked { gst::debug!( CAT, imp = self, "Not scheduling source restart timeout because we are manually blocked", ); return; } let clock = gst::SystemClock::obtain(); let wait_time = clock.time() + state.settings.restart_timeout - elapsed; gst::debug!( CAT, imp = self, "Scheduling {}source restart timeout for {}", if fallback_source { "fallback " } else { "" }, wait_time, ); let timeout = clock.new_single_shot_id(wait_time); let element_weak = self.obj().downgrade(); timeout .wait_async(move |_clock, _time, _id| { let Some(element) = element_weak.upgrade() else { return; }; element.call_async(move |element| { let imp = element.imp(); gst::debug!( CAT, imp = imp, "{}source restart timeout triggered", if fallback_source { "fallback " } else { "" } ); let mut state_guard = imp.state.lock(); let state = match &mut *state_guard { None => { gst::debug!( CAT, imp = imp, "Restarting {}source not needed anymore", if fallback_source { "fallback " } else { "" } ); return; } Some(state) => state, }; let source = if fallback_source { if let Some(ref mut source) = state.fallback_source { source } else { return; } } else { &mut state.source }; source.restart_timeout = None; // If we have the fallback activated then restart the source now. if fallback_source || imp.all_pads_fallback_activated(state) { let (last_buffering_update, buffering_percent) = if fallback_source { ( state.fallback_last_buffering_update, state.stats.fallback_buffering_percent, ) } else { (state.last_buffering_update, state.stats.buffering_percent) }; // If we're not actively buffering right now let's restart the source if last_buffering_update .map(|i| i.elapsed() >= state.settings.restart_timeout.into()) .unwrap_or(buffering_percent == 100) { gst::debug!( CAT, imp = imp, "Not buffering, restarting {}source", if fallback_source { "fallback " } else { "" } ); imp.handle_source_error(state, RetryReason::Timeout, fallback_source); drop(state_guard); element.notify("statistics"); } else { gst::debug!( CAT, imp = imp, "Buffering, restarting {}source later", if fallback_source { "fallback " } else { "" } ); let elapsed = last_buffering_update .and_then(|last_buffering_update| { gst::ClockTime::try_from(last_buffering_update.elapsed()).ok() }) .unwrap_or(gst::ClockTime::ZERO); imp.schedule_source_restart_timeout(state, elapsed, fallback_source); } } else { gst::debug!( CAT, imp = imp, "Restarting {}source not needed anymore", if fallback_source { "fallback " } else { "" } ); } }); }) .expect("Failed to wait async"); source.restart_timeout = Some(timeout); } #[allow(clippy::blocks_in_conditions)] fn all_pads_fallback_activated(&self, state: &State) -> bool { // If we have no streams yet, or all active pads for the ones we have // are the fallback pads. if state.source.posted_streams.is_some() { state .streams .iter() .filter_map(|s| s.output.as_ref()) .all(|o| { let pad = o.switch.property::>("active-pad").unwrap(); pad.property::("priority") != 0 }) } else { true } } #[allow(clippy::blocks_in_conditions)] fn has_fallback_activated(&self, state: &State, gst_stream: &gst::Stream) -> bool { // This is only called from handle_switch_active_pad_change() below, // so we can assume that the stream mentioned does have an output let stream = state .streams .iter() .find(|s| s.gst_stream.stream_id() == gst_stream.stream_id()) .unwrap(); let output = stream.output.as_ref().unwrap(); output .switch .property::>("active-pad") .unwrap() .property::("priority") != 0 } fn handle_switch_active_pad_change(&self, stream: &gst::Stream) { let mut state_guard = self.state.lock(); let state = match &mut *state_guard { None => { return; } Some(state) => state, }; // If we have the fallback activated then start the retry timeout once // unless it was started already. Otherwise cancel the retry timeout. // If not all pads have fallback activated by the time this timeout triggers, // (e.g. we have a main source, but with less streams than the user selected) // the source will not be restarted. if self.has_fallback_activated(state, stream) { gst::warning!( CAT, imp = self, "Switched to {} fallback stream", stream.stream_id().unwrap() ); if state.source.restart_timeout.is_none() { self.schedule_source_restart_timeout(state, gst::ClockTime::ZERO, false); } } else { gst::debug!( CAT, imp = self, "Switched to {} main stream", stream.stream_id().unwrap() ); if let Some(timeout) = state.source.retry_timeout.take() { gst::debug!(CAT, imp = self, "Unscheduling retry timeout"); timeout.unschedule(); } if let Some(timeout) = state.source.restart_timeout.take() { gst::debug!(CAT, imp = self, "Unscheduling restart timeout"); timeout.unschedule(); } } drop(state_guard); self.obj().notify("status"); } fn stats(&self) -> gst::Structure { let state_guard = self.state.lock(); let state = match &*state_guard { None => return Stats::default().to_structure(), Some(ref state) => state, }; state.stats.to_structure() } } gst-plugin-fallbackswitch-0.14.3/src/fallbacksrc/mod.rs000064400000000000000000000022251046102023000211450ustar 00000000000000// Copyright (C) 2020 Sebastian Dröge // // This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. // If a copy of the MPL was not distributed with this file, You can obtain one at // . // // SPDX-License-Identifier: MPL-2.0 use gst::glib; use gst::prelude::*; mod custom_source; mod imp; #[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, glib::Enum)] #[repr(u32)] #[enum_type(name = "GstFallbackSourceRetryReason")] pub enum RetryReason { None, Error, Eos, StateChangeFailure, Timeout, } #[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Clone, Copy, glib::Enum)] #[repr(u32)] #[enum_type(name = "GstFallbackSourceStatus")] pub enum Status { Stopped, Buffering, Retrying, Running, } glib::wrapper! { pub struct FallbackSrc(ObjectSubclass) @extends gst::Bin, gst::Element, gst::Object; } pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { gst::Element::register( Some(plugin), "fallbacksrc", gst::Rank::NONE, FallbackSrc::static_type(), ) } gst-plugin-fallbackswitch-0.14.3/src/fallbackswitch/imp.rs000064400000000000000000001632471046102023000217010ustar 00000000000000// Copyright (C) 2020 Mathieu Duponchelle // Copyright (C) 2021 Jan Schmidt // // This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. // If a copy of the MPL was not distributed with this file, You can obtain one at // . // // SPDX-License-Identifier: MPL-2.0 use gst::glib; use gst::prelude::*; use gst::subclass::prelude::*; use gst::{debug, log, trace}; use std::sync::LazyLock; use parking_lot::{Condvar, Mutex, MutexGuard}; use std::sync::atomic::{AtomicU32, Ordering}; const PROP_PRIORITY: &str = "priority"; const PROP_IS_HEALTHY: &str = "is-healthy"; const PROP_ACTIVE_PAD: &str = "active-pad"; const PROP_AUTO_SWITCH: &str = "auto-switch"; const PROP_IMMEDIATE_FALLBACK: &str = "immediate-fallback"; const PROP_LATENCY: &str = "latency"; const PROP_MIN_UPSTREAM_LATENCY: &str = "min-upstream-latency"; const PROP_TIMEOUT: &str = "timeout"; const PROP_STOP_ON_EOS: &str = "stop-on-eos"; static CAT: LazyLock = LazyLock::new(|| { gst::DebugCategory::new( "fallbackswitch", gst::DebugColorFlags::empty(), Some("Automatic priority-based input selector"), ) }); /* Mutex locking ordering: - self.settings - self.state - self.active_sinkpad - pad.settings - pad.state */ #[derive(Debug)] #[allow(clippy::large_enum_variant)] enum CapsInfo { None, Audio(gst_audio::AudioInfo), Video(gst_video::VideoInfo), } #[derive(Clone, Debug)] struct Settings { timeout: gst::ClockTime, latency: gst::ClockTime, min_upstream_latency: gst::ClockTime, immediate_fallback: bool, auto_switch: bool, stop_on_eos: bool, } impl Default for Settings { fn default() -> Settings { Settings { timeout: gst::ClockTime::SECOND, latency: gst::ClockTime::ZERO, min_upstream_latency: gst::ClockTime::ZERO, immediate_fallback: false, auto_switch: true, stop_on_eos: false, } } } #[derive(Debug)] struct State { upstream_latency: gst::ClockTime, timed_out: bool, switched_pad: bool, discont_pending: bool, first: bool, output_running_time: Option, timeout_running_time: Option, timeout_clock_id: Option, /// If the src pad is currently busy. Should be checked and waited on using `src_busy_cond` /// before calling anything requiring the stream lock. src_busy: bool, } impl Default for State { fn default() -> State { State { upstream_latency: gst::ClockTime::ZERO, timed_out: false, switched_pad: false, discont_pending: true, first: true, output_running_time: None, timeout_running_time: None, timeout_clock_id: None, src_busy: false, } } } impl State { fn cancel_timeout(&mut self) { /* clear any previous timeout */ if let Some(clock_id) = self.timeout_clock_id.take() { clock_id.unschedule(); } } } impl Drop for State { fn drop(&mut self) { self.cancel_timeout(); } } #[derive(Debug)] pub struct FallbackSwitchSinkPad { state: Mutex, settings: Mutex, } #[glib::object_subclass] impl ObjectSubclass for FallbackSwitchSinkPad { const NAME: &'static str = "GstFallbackSwitchSinkPad"; type Type = super::FallbackSwitchSinkPad; type ParentType = gst::Pad; fn new() -> Self { Self { state: Mutex::new(SinkState::default()), settings: Mutex::new(SinkSettings::default()), } } } impl GstObjectImpl for FallbackSwitchSinkPad {} impl ObjectImpl for FallbackSwitchSinkPad { fn properties() -> &'static [glib::ParamSpec] { static PROPERTIES: LazyLock> = LazyLock::new(|| { vec![ glib::ParamSpecUInt::builder(PROP_PRIORITY) .nick("Stream Priority") .blurb( "Selection priority for this stream (lower number has a higher priority)", ) .default_value(SinkSettings::default().priority) .build(), glib::ParamSpecBoolean::builder(PROP_IS_HEALTHY) .nick("Stream Health") .blurb("Whether this stream is healthy") .default_value(false) .read_only() .build(), ] }); PROPERTIES.as_ref() } fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { match pspec.name() { PROP_PRIORITY => { let mut settings = self.settings.lock(); let priority = value.get().expect("type checked upstream"); settings.priority = priority; } _ => unimplemented!(), } } fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { match pspec.name() { PROP_PRIORITY => { let settings = self.settings.lock(); settings.priority.to_value() } PROP_IS_HEALTHY => { let state = self.state.lock(); state.is_healthy.to_value() } _ => unimplemented!(), } } } impl PadImpl for FallbackSwitchSinkPad {} impl FallbackSwitchSinkPad {} #[derive(Clone, Debug, Default)] struct SinkSettings { priority: u32, } #[derive(Debug)] struct SinkState { is_healthy: bool, segment: gst::FormattedSegment, caps_info: CapsInfo, current_running_time: Option, flushing: bool, clock_id: Option, /// true if the sink pad has received eos eos: bool, } impl Default for SinkState { fn default() -> Self { Self { is_healthy: false, segment: gst::FormattedSegment::new(), caps_info: CapsInfo::None, current_running_time: gst::ClockTime::NONE, flushing: false, clock_id: None, eos: false, } } } impl SinkState { fn flush_start(&mut self) { self.flushing = true; if let Some(clock_id) = self.clock_id.take() { clock_id.unschedule(); } } fn cancel_wait(&mut self) { if let Some(clock_id) = self.clock_id.take() { clock_id.unschedule(); } } fn reset(&mut self) { self.flushing = false; self.caps_info = CapsInfo::None; self.eos = false; } fn clip_buffer(&self, mut buffer: gst::Buffer) -> Option { match &self.caps_info { CapsInfo::Audio(audio_info) => gst_audio::audio_buffer_clip( buffer, self.segment.upcast_ref(), audio_info.rate(), audio_info.bpf(), ), CapsInfo::Video(video_info) => { let start_ts = buffer.pts(); let duration = buffer.duration().or_else(|| { if video_info.fps().numer() > 0 { gst::ClockTime::SECOND.mul_div_floor( video_info.fps().denom() as u64, video_info.fps().numer() as u64, ) } else { None } }); let end_ts = start_ts.opt_saturating_add(duration); let (clipped_start_ts, clipped_end_ts) = self.segment.clip(start_ts, end_ts)?; let clipped_duration = clipped_end_ts.opt_sub(clipped_start_ts); if clipped_start_ts != start_ts || clipped_duration != buffer.duration() { let buffer = buffer.make_mut(); buffer.set_pts(clipped_start_ts); buffer.set_duration(clipped_duration); } Some(buffer) } CapsInfo::None => { let start_ts = buffer.pts(); let end_ts = start_ts.opt_saturating_add(buffer.duration()); // Can only clip buffers completely away, i.e. drop them, if they're raw if let Some((clipped_start_ts, clipped_end_ts)) = self.segment.clip(start_ts, end_ts) { let clipped_duration = clipped_end_ts.opt_sub(clipped_start_ts); if clipped_start_ts != start_ts || clipped_duration != buffer.duration() { let buffer = buffer.make_mut(); buffer.set_pts(clipped_start_ts); buffer.set_duration(clipped_duration); } } Some(buffer) } } } fn get_sync_time( &self, buffer: &gst::Buffer, ) -> (Option, Option) { let last_ts = self.current_running_time; let duration = buffer.duration().unwrap_or(gst::ClockTime::ZERO); let start_ts = match buffer.dts_or_pts() { Some(ts) => ts, None => return (last_ts, last_ts), }; let end_ts = start_ts.saturating_add(duration); match self.segment.clip(start_ts, end_ts) { Some((start_ts, end_ts)) => ( self.segment.to_running_time(start_ts), self.segment.to_running_time(end_ts), ), None => (None, None), } } fn schedule_clock( &mut self, imp: &FallbackSwitch, pad: &super::FallbackSwitchSinkPad, running_time: Option, extra_time: gst::ClockTime, ) -> Option { let running_time = running_time?; let clock = imp.obj().clock()?; let base_time = imp.obj().base_time()?; let wait_until = running_time + base_time; let wait_until = wait_until.saturating_add(extra_time); let now = clock.time(); /* If the buffer is already late, skip the clock wait */ if wait_until < now { debug!( CAT, obj = pad, "Skipping buffer wait until {} - clock already {}", wait_until, now ); return None; } debug!( CAT, obj = pad, "Scheduling buffer wait until {} = {} + extra {} + base time {}", wait_until, running_time, extra_time, base_time ); let clock_id = clock.new_single_shot_id(wait_until); self.clock_id = Some(clock_id.clone()); Some(clock_id) } fn is_healthy( &self, pad: &super::FallbackSwitchSinkPad, state: &State, settings: &Settings, now_running_time: Option, ) -> bool { /* The pad is healthy if it has received data within the * last 'timeout' duration, which means the pad's current_running_time+timeout * is later than 'now' according to the passed in running time, but not later * than the timeout_running_time that would mean we time out before outputting * that buffer */ match ( self.current_running_time, now_running_time, state.timeout_running_time, ) { (Some(pad_running_time), Some(now_running_time), Some(global_timeout_running_time)) => { let timeout_running_time = pad_running_time.saturating_add(settings.timeout); log!( CAT, obj = pad, "pad_running_time {} timeout_running_time {} now_running_time {}", pad_running_time, timeout_running_time, now_running_time, ); timeout_running_time > now_running_time // Must be > not >= && pad_running_time <= global_timeout_running_time } (Some(pad_running_time), Some(now_running_time), None) => { let timeout_running_time = pad_running_time.saturating_add(settings.timeout); log!( CAT, obj = pad, "pad_running_time {} timeout_running_time {} now_running_time {}", pad_running_time, timeout_running_time, now_running_time, ); timeout_running_time > now_running_time // Must be > not >= } (Some(_input_running_time), None, _) => true, (None, _, _) => false, } } } #[derive(Debug)] pub struct FallbackSwitch { state: Mutex, src_busy_cond: Condvar, settings: Mutex, // Separated from the rest of the `state` because it can be // read from a property notify, which is prone to deadlocks. // https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/200 active_sinkpad: Mutex>, src_pad: gst::Pad, sink_pad_serial: AtomicU32, } impl GstObjectImpl for FallbackSwitch {} impl FallbackSwitch { fn set_active_pad(&self, state: &mut State, pad: &super::FallbackSwitchSinkPad) { let prev_active_pad = self.active_sinkpad.lock().replace(pad.clone()); if prev_active_pad.as_ref() == Some(pad) { return; } state.switched_pad = true; state.discont_pending = true; let mut pad_state = pad.imp().state.lock(); pad_state.cancel_wait(); drop(pad_state); debug!(CAT, obj = pad, "Now active pad"); } fn handle_timeout(&self, state: &mut State, settings: &Settings) { debug!( CAT, imp = self, "timeout fired - looking for a pad to switch to" ); /* Advance the output running time to this timeout */ state.output_running_time = state.timeout_running_time; if !settings.auto_switch { /* If auto-switching is disabled, don't check for a new * pad */ state.timed_out = true; return; } let active_sinkpad = self.active_sinkpad.lock().clone(); let mut best_priority = 0u32; let mut best_pad = None; let now_running_time = state.timeout_running_time; for pad in self.obj().sink_pads() { /* Don't consider the active sinkpad */ let pad = pad.downcast_ref::().unwrap(); let pad_imp = pad.imp(); if active_sinkpad.as_ref() == Some(pad) { continue; } let pad_settings = pad_imp.settings.lock().clone(); let pad_state = pad_imp.state.lock(); #[allow(clippy::collapsible_if)] /* If this pad has data that arrived within the 'timeout' window * before the timeout fired, we can switch to it */ if pad_state.is_healthy(pad, state, settings, now_running_time) { if best_pad.is_none() || pad_settings.priority < best_priority { best_pad = Some(pad.clone()); best_priority = pad_settings.priority; } } } if let Some(best_pad) = best_pad { debug!( CAT, imp = self, "Found viable pad to switch to: {:?}", best_pad ); self.set_active_pad(state, &best_pad) } else { state.timed_out = true; } } fn on_timeout(&self, clock_id: &gst::ClockId) { let settings = self.settings.lock().clone(); let mut state = self.state.lock(); if state.timeout_clock_id.as_ref() != Some(clock_id) { /* Timeout fired late, ignore it. */ debug!(CAT, imp = self, "Late timeout callback. Ignoring"); return; } // Ensure sink_chain on an inactive pad can schedule another timeout state.timeout_clock_id = None; self.handle_timeout(&mut state, &settings); let changed = self.update_health_statuses(&state, &settings); drop(state); for pad in changed { pad.notify(PROP_IS_HEALTHY); } } fn cancel_waits(&self) { for pad in self.obj().sink_pads() { let pad = pad.downcast_ref::().unwrap(); let pad_imp = pad.imp(); let mut pad_state = pad_imp.state.lock(); pad_state.cancel_wait(); } } fn schedule_timeout( &self, state: &mut State, settings: &Settings, running_time: gst::ClockTime, ) -> bool { state.cancel_timeout(); let clock = match self.obj().clock() { None => return false, Some(clock) => clock, }; let base_time = match self.obj().base_time() { Some(base_time) => base_time, None => return false, }; let timeout_running_time = running_time .saturating_add(state.upstream_latency + settings.timeout + settings.latency); let wait_until = timeout_running_time + base_time; state.timeout_running_time = Some(timeout_running_time); /* If we're already running behind, fire the timeout immediately */ let now = clock.time(); if wait_until <= now { self.handle_timeout(state, settings); return true; } debug!(CAT, imp = self, "Scheduling timeout for {}", wait_until); let timeout_id = clock.new_single_shot_id(wait_until); state.timeout_clock_id = Some(timeout_id.clone().into()); state.timed_out = false; let imp_weak = self.downgrade(); timeout_id .wait_async(move |_clock, _time, clock_id| { let Some(imp) = imp_weak.upgrade() else { return; }; imp.on_timeout(clock_id); }) .expect("Failed to wait async"); false } fn update_health_statuses( &self, state: &State, settings: &Settings, ) -> Vec { let mut changed = Vec::::new(); /* Iterate over sink pads and update their is_healthy status, * returning a Vec of pads whose health changed and need notifying */ for pad in self.obj().sink_pads() { let pad = pad.downcast_ref::().unwrap(); let pad_imp = pad.imp(); let mut pad_state = pad_imp.state.lock(); /* If this pad has data that arrived within the 'timeout' window * before the timeout fired, we can switch to it */ let is_healthy = pad_state.is_healthy(pad, state, settings, state.output_running_time); let health_changed = is_healthy != pad_state.is_healthy; pad_state.is_healthy = is_healthy; drop(pad_state); if health_changed { log!(CAT, obj = pad, "Health changed to {}", is_healthy); changed.push(pad.clone()); } } changed } fn sink_activatemode( pad: &super::FallbackSwitchSinkPad, _mode: gst::PadMode, activate: bool, ) -> Result<(), gst::LoggableError> { let mut pad_state = pad.imp().state.lock(); if activate { pad_state.reset(); } else { pad_state.flush_start(); } Ok(()) } fn sink_chain( &self, pad: &super::FallbackSwitchSinkPad, buffer: gst::Buffer, ) -> Result { self.chain(pad, buffer, None) } fn chain( &self, pad: &super::FallbackSwitchSinkPad, buffer: gst::Buffer, from_gap: Option<&gst::event::Gap>, ) -> Result { let settings = self.settings.lock().clone(); let mut state = self.state.lock(); let pad = pad.downcast_ref::().unwrap(); let pad_imp = pad.imp(); if settings.stop_on_eos && self.has_sink_pad_eos() { debug!(CAT, obj = pad, "return eos as stop-on-eos is enabled"); return Err(gst::FlowError::Eos); } let mut buffer = { let pad_state = pad_imp.state.lock(); trace!( CAT, obj = pad, "Clipping {:?} against segment {:?}", buffer, pad_state.segment, ); match pad_state.clip_buffer(buffer) { Some(buffer) => buffer, None => { log!( CAT, obj = pad, "Dropping raw buffer completely out of segment", ); return Ok(gst::FlowSuccess::Ok); } } }; /* There are 4 cases coming in: * 1. This is not the active pad but is higher priority: * - become the active pad, then goto 4. * 2. This is not the active pad, but the output timed out due to all pads running * late. * - become the active pad, then goto 4. * 3. This is not the active pad, but might become the active pad * - Wait for the buffer end time (or buffer start time + timeout if there's no * duration). If we get woken early, and became the active pad, then output the * buffer. * 4. This is the active pad: * - sleep until the buffer running time, then check if we're still active */ /* see if we should become the active pad */ let active_sinkpad = self.active_sinkpad.lock().clone(); let mut is_active = active_sinkpad.as_ref() == Some(pad); if !is_active && settings.auto_switch { let pad_settings = pad_imp.settings.lock().clone(); let mut switch_to_pad = state.timed_out; switch_to_pad |= if let Some(active_sinkpad) = &active_sinkpad { let active_sinkpad_imp = active_sinkpad.imp(); let active_pad_settings = active_sinkpad_imp.settings.lock().clone(); (pad_settings.priority < active_pad_settings.priority) || (state.first && settings.immediate_fallback) } else { match settings.immediate_fallback { true => true, false => pad_settings.priority == 0, } }; if state.first { state.first = false; } if switch_to_pad { state.timed_out = false; self.set_active_pad(&mut state, pad); is_active = true; } } let mut pad_state = pad_imp.state.lock(); let raw_pad = !matches!(pad_state.caps_info, CapsInfo::None); let (start_running_time, end_running_time) = pad_state.get_sync_time(&buffer); if let Some(running_time) = start_running_time { pad_state.current_running_time = Some(running_time); } /* Update pad is-healthy state if necessary and notify * if it changes, as that might affect which pad is * active */ let is_healthy = pad_state.is_healthy(pad, &state, &settings, state.output_running_time); let health_changed = is_healthy != pad_state.is_healthy; pad_state.is_healthy = is_healthy; /* Need to drop state locks before notifying */ let (mut state, mut pad_state) = if health_changed { drop(pad_state); drop(state); log!(CAT, obj = pad, "Health changed to {}", is_healthy); pad.notify(PROP_IS_HEALTHY); if !settings.auto_switch { /* Re-check if this is the active sinkpad */ let active_sinkpad = self.active_sinkpad.lock().clone(); is_active = active_sinkpad.as_ref() == Some(pad); } (self.state.lock(), pad_imp.state.lock()) } else { (state, pad_state) }; log!( CAT, obj = pad, "Handling {:?} run ts start {} end {} pad active {}", buffer, start_running_time.display(), end_running_time.display(), is_active ); #[allow(clippy::blocks_in_conditions)] let output_clockid = if is_active { pad_state.schedule_clock( self, pad, start_running_time, state.upstream_latency + settings.latency, ) } else if state.output_running_time.is_some() && end_running_time.is_some_and(|end_running_time| { end_running_time < state.output_running_time.unwrap() }) { if raw_pad { log!( CAT, obj = pad, "Dropping trailing raw {:?} before timeout {}", buffer, state.timeout_running_time.unwrap() ); return Ok(gst::FlowSuccess::Ok); } else { log!( CAT, obj = pad, "Not dropping trailing non-raw {:?} before timeout {}", buffer, state.timeout_running_time.unwrap() ); None } } else { pad_state.schedule_clock( self, pad, end_running_time, state.upstream_latency + settings.timeout + settings.latency, ) }; drop(pad_state); let mut update_all_pad_health = false; /* Before sleeping, ensure there is a timeout to switch active pads, * in case the initial active pad never receives a buffer */ if let Some(running_time) = start_running_time { if state.timeout_clock_id.is_none() && !is_active { // May change active pad immediately update_all_pad_health = self.schedule_timeout(&mut state, &settings, running_time); is_active = self.active_sinkpad.lock().as_ref() == Some(pad); } } if let Some(clock_id) = &output_clockid { MutexGuard::unlocked(&mut state, || { let (_res, _) = clock_id.wait(); }); is_active = self.active_sinkpad.lock().as_ref() == Some(pad); } let pad_state = pad_imp.state.lock(); if pad_state.flushing { debug!(CAT, imp = self, "Flushing"); return Err(gst::FlowError::Flushing); } // calling schedule_timeout() may result in handle_timeout() being called right away, // which will need pad state locks, so drop it now to prevent deadlocks. drop(pad_state); if is_active { if start_running_time .opt_lt(state.output_running_time) .unwrap_or(false) { if raw_pad { log!( CAT, obj = pad, "Dropping trailing raw {:?} before output running time {}", buffer, state.output_running_time.display(), ); return Ok(gst::FlowSuccess::Ok); } else { log!( CAT, obj = pad, "Not dropping trailing non-raw {:?} before output running time {}", buffer, state.output_running_time.display(), ); } } if let Some(start_running_time) = start_running_time { if let Some(output_running_time) = state.output_running_time { state.output_running_time = Some(std::cmp::max(start_running_time, output_running_time)); } else { state.output_running_time = Some(start_running_time); } } if let Some(end_running_time) = end_running_time { // May change active pad immediately update_all_pad_health |= self.schedule_timeout(&mut state, &settings, end_running_time); is_active = self.active_sinkpad.lock().as_ref() == Some(pad); } else { state.cancel_timeout(); } } let mut pad_state = pad_imp.state.lock(); if let Some(running_time) = end_running_time { pad_state.current_running_time = Some(running_time); } let is_healthy = pad_state.is_healthy(pad, &state, &settings, state.output_running_time); let health_changed = is_healthy != pad_state.is_healthy; if health_changed { log!(CAT, obj = pad, "Health changed to {}", is_healthy); } pad_state.is_healthy = is_healthy; drop(pad_state); /* If the schedule_timeout() calls above said the timeout happened, * we should update the health of all pads here */ let mut state = if update_all_pad_health { let changed_health_pads = self.update_health_statuses(&state, &settings); drop(state); for pad in changed_health_pads { pad.notify(PROP_IS_HEALTHY); } self.state.lock() } else { state }; if !is_active { log!(CAT, obj = pad, "Dropping {:?} on inactive pad", buffer); drop(state); if health_changed { pad.notify(PROP_IS_HEALTHY); } return Ok(gst::FlowSuccess::Ok); } // Lock order: First stream lock then state lock! let _stream_lock = MutexGuard::unlocked(&mut state, || self.src_pad.stream_lock()); is_active = self.active_sinkpad.lock().as_ref() == Some(pad); if !is_active { log!(CAT, obj = pad, "Dropping {:?} on inactive pad", buffer); drop(state); if health_changed { pad.notify(PROP_IS_HEALTHY); } return Ok(gst::FlowSuccess::Ok); } /* Update the health status for all pads, since we're the active pad */ let changed_health_pads = self.update_health_statuses(&state, &settings); let switched_pad = state.switched_pad; let discont_pending = state.discont_pending; state.switched_pad = false; state.discont_pending = false; drop(state); if health_changed { pad.notify(PROP_IS_HEALTHY); } for pad in changed_health_pads { pad.notify(PROP_IS_HEALTHY); } if switched_pad { self.with_src_busy(|| { let _ = pad.push_event(gst::event::Reconfigure::new()); pad.sticky_events_foreach(|event| { self.src_pad.push_event(event.clone()); std::ops::ControlFlow::Continue(gst::EventForeachAction::Keep) }); }); self.obj().notify(PROP_ACTIVE_PAD); } if discont_pending && !buffer.flags().contains(gst::BufferFlags::DISCONT) { let buffer = buffer.make_mut(); buffer.set_flags(gst::BufferFlags::DISCONT); } /* TODO: Clip raw video and audio buffers to avoid going backward? */ log!(CAT, obj = pad, "Forwarding {:?}", buffer); if let Some(in_gap_event) = from_gap { // Safe unwrap: the buffer was constructed from a gap event with // a timestamp, and even if its timestamp was adjusted it should never // be NONE by now let pts = buffer.pts().unwrap(); let out_gap_event = { #[cfg(feature = "v1_20")] { gst::event::Gap::builder(pts) .duration(buffer.duration()) .seqnum(in_gap_event.seqnum()) .gap_flags(in_gap_event.gap_flags()) .build() } #[cfg(not(feature = "v1_20"))] { gst::event::Gap::builder(pts) .duration(buffer.duration()) .seqnum(in_gap_event.seqnum()) .build() } }; self.with_src_busy(|| { self.src_pad.push_event(out_gap_event); }); Ok(gst::FlowSuccess::Ok) } else { self.with_src_busy(|| self.src_pad.push(buffer)) } } fn sink_chain_list( &self, pad: &super::FallbackSwitchSinkPad, list: gst::BufferList, ) -> Result { log!(CAT, obj = pad, "Handling buffer list {:?}", list); // TODO: Keep the list intact and forward it in one go (or broken into several // pieces if needed) when outputting to the active pad for buffer in list.iter_owned() { self.chain(pad, buffer, None)?; } Ok(gst::FlowSuccess::Ok) } fn sink_event(&self, pad: &super::FallbackSwitchSinkPad, event: gst::Event) -> bool { log!(CAT, obj = pad, "Handling event {:?}", event); if let gst::EventView::Gap(ev) = event.view() { let mut buffer = gst::Buffer::new(); { let buf_mut = buffer.get_mut().unwrap(); buf_mut.set_flags(gst::BufferFlags::GAP); let (pts, duration) = ev.get(); buf_mut.set_pts(pts); buf_mut.set_duration(duration); } return match self.chain(pad, buffer, Some(ev)) { Ok(_) => true, Err(gst::FlowError::Flushing) | Err(gst::FlowError::Eos) => true, Err(err) => { gst::error!(CAT, obj = pad, "Error processing gap event: {}", err); false } }; } let mut state = self.state.lock(); let mut pad_state = pad.imp().state.lock(); match event.view() { gst::EventView::Caps(caps) => { let caps = caps.caps(); debug!(CAT, obj = pad, "Received caps {}", caps); let caps_info = match caps.structure(0).unwrap().name().as_str() { "audio/x-raw" => { CapsInfo::Audio(gst_audio::AudioInfo::from_caps(caps).unwrap()) } "video/x-raw" => { CapsInfo::Video(gst_video::VideoInfo::from_caps(caps).unwrap()) } _ => CapsInfo::None, }; pad_state.caps_info = caps_info; } gst::EventView::Segment(e) => { let segment = match e.segment().clone().downcast::() { Err(segment) => { gst::element_imp_error!( self, gst::StreamError::Format, ["Only TIME segments supported, got {:?}", segment.format(),] ); return false; } Ok(segment) => segment, }; pad_state.segment = segment; } gst::EventView::FlushStart(_) => { pad_state.flush_start(); } gst::EventView::FlushStop(_) => { pad_state.reset(); state.first = true; } gst::EventView::Eos(_) => { pad_state.eos = true; } gst::EventView::StreamStart(_) => { pad_state.eos = false; } _ => {} } drop(pad_state); let mut is_active = self.active_sinkpad.lock().as_ref() == Some(pad); if !is_active { log!(CAT, obj = pad, "Dropping {:?} on inactive pad", event); return true; } // Lock order: First stream lock then state lock! let stream_lock_for_serialized = event .is_serialized() .then(|| MutexGuard::unlocked(&mut state, || self.src_pad.stream_lock())); is_active = self.active_sinkpad.lock().as_ref() == Some(pad); if !is_active { log!(CAT, obj = pad, "Dropping {:?} on inactive pad", event); return true; } let fwd_sticky = if state.switched_pad && stream_lock_for_serialized.is_some() { state.switched_pad = false; true } else { false }; drop(state); if fwd_sticky { self.with_src_busy(|| { let _ = pad.push_event(gst::event::Reconfigure::new()); pad.sticky_events_foreach(|event| { self.src_pad.push_event(event.clone()); std::ops::ControlFlow::Continue(gst::EventForeachAction::Keep) }); }); self.obj().notify(PROP_ACTIVE_PAD); } self.with_src_busy(|| self.src_pad.push_event(event)) } fn sink_query(&self, pad: &super::FallbackSwitchSinkPad, query: &mut gst::QueryRef) -> bool { use gst::QueryView; log!(CAT, obj = pad, "Handling query {:?}", query); let forward = match query.view() { QueryView::Context(_) => true, QueryView::Position(_) => true, QueryView::Duration(_) => true, QueryView::Caps(_) => true, QueryView::Allocation(_) => { /* Forward allocation only for the active sink pad, * for others switching will send a reconfigure event upstream */ self.active_sinkpad.lock().as_ref() == Some(pad) } _ => { gst::Pad::query_default(pad, Some(&*self.obj()), query); false } }; if forward { log!(CAT, obj = pad, "Forwarding query {:?}", query); self.src_pad.peer_query(query) } else { false } } fn reset(&self) { let mut state = self.state.lock(); *state = State::default(); self.active_sinkpad.lock().take(); } fn src_query(&self, pad: &gst::Pad, query: &mut gst::QueryRef) -> bool { use gst::QueryViewMut; log!(CAT, obj = pad, "Handling {:?}", query); match query.view_mut() { QueryViewMut::Latency(ref mut q) => { let mut ret = true; let mut min_latency = gst::ClockTime::ZERO; let mut max_latency = gst::ClockTime::NONE; for pad in self.obj().sink_pads() { let mut peer_query = gst::query::Latency::new(); ret = pad.peer_query(&mut peer_query); if ret { let (live, min, max) = peer_query.result(); if live { min_latency = min.max(min_latency); max_latency = max .zip(max_latency) .map(|(max, max_latency)| max.min(max_latency)) .or(max); } } } let settings = self.settings.lock().clone(); let mut state = self.state.lock(); min_latency = min_latency.max(settings.min_upstream_latency); state.upstream_latency = min_latency; log!(CAT, obj = pad, "Upstream latency {}", min_latency); q.set(true, min_latency + settings.latency, max_latency); ret } QueryViewMut::Caps(_) => { // Unlock before forwarding let sinkpad = self.active_sinkpad.lock().clone(); if let Some(sinkpad) = sinkpad { sinkpad.peer_query(query) } else { gst::Pad::query_default(pad, Some(&*self.obj()), query) } } _ => { // Unlock before forwarding let sinkpad = self.active_sinkpad.lock().clone(); if let Some(sinkpad) = sinkpad { sinkpad.peer_query(query) } else { true } } } } /// check if at least one sink pad has received eos fn has_sink_pad_eos(&self) -> bool { let pads = self.obj().sink_pads(); for pad in pads { let pad = pad.downcast_ref::().unwrap(); let pad_imp = pad.imp(); let pad_state = pad_imp.state.lock(); if pad_state.eos { return true; } } false } /// Wait until src_busy is not set and set it, execute /// the closure, then unset it again and notify its Cond. /// /// The State lock is taken while modifying src_busy, /// but not while executing the closure. fn with_src_busy(&self, func: F) -> R where F: FnOnce() -> R, { { let mut state = self.state.lock(); while state.src_busy { self.src_busy_cond.wait(&mut state); } state.src_busy = true; } let ret = func(); { let mut state = self.state.lock(); state.src_busy = false; self.src_busy_cond.notify_one(); } ret } } #[glib::object_subclass] impl ObjectSubclass for FallbackSwitch { const NAME: &'static str = "GstFallbackSwitch"; type Type = super::FallbackSwitch; type ParentType = gst::Element; type Interfaces = (gst::ChildProxy,); fn with_class(klass: &Self::Class) -> Self { let templ = klass.pad_template("src").unwrap(); let srcpad = gst::Pad::builder_from_template(&templ) .query_function(|pad, parent, query| { FallbackSwitch::catch_panic_pad_function( parent, || false, |fallbackswitch| fallbackswitch.src_query(pad, query), ) }) .build(); Self { state: Mutex::new(State::default()), src_busy_cond: Condvar::default(), settings: Mutex::new(Settings::default()), active_sinkpad: Mutex::new(None), src_pad: srcpad, sink_pad_serial: AtomicU32::new(0), } } } impl ObjectImpl for FallbackSwitch { fn properties() -> &'static [glib::ParamSpec] { static PROPERTIES: LazyLock> = LazyLock::new(|| { vec![ glib::ParamSpecObject::builder::(PROP_ACTIVE_PAD) .nick("Active Pad") .blurb("Currently active pad") .mutable_playing() .build(), glib::ParamSpecUInt64::builder(PROP_TIMEOUT) .nick("Input timeout") .blurb("Timeout on an input before switching to a lower priority input.") .maximum(u64::MAX - 1) .default_value(Settings::default().timeout.nseconds()) .mutable_playing() .build(), glib::ParamSpecUInt64::builder(PROP_LATENCY) .nick("Latency") .blurb("Additional latency in live mode to allow upstream to take longer to produce buffers for the current position (in nanoseconds)") .maximum(u64::MAX - 1) .default_value(Settings::default().latency.nseconds()) .mutable_ready() .build(), glib::ParamSpecUInt64::builder(PROP_MIN_UPSTREAM_LATENCY) .nick("Minimum Upstream Latency") .blurb("When sources with a higher latency are expected to be plugged in dynamically after the fallbackswitch has started playing, this allows overriding the minimum latency reported by the initial source(s). This is only taken into account when larger than the actually reported minimum latency. (nanoseconds)") .maximum(u64::MAX - 1) .default_value(Settings::default().min_upstream_latency.nseconds()) .mutable_ready() .build(), glib::ParamSpecBoolean::builder(PROP_IMMEDIATE_FALLBACK) .nick("Immediate fallback") .blurb("Forward lower-priority streams immediately at startup, when the stream with priority 0 is slow to start up and immediate output is required") .default_value(Settings::default().immediate_fallback) .mutable_ready() .build(), glib::ParamSpecBoolean::builder(PROP_AUTO_SWITCH) .nick("Automatically switch pads") .blurb("Automatically switch pads (If true, use the priority pad property, otherwise manual selection via the active-pad property)") .default_value(Settings::default().auto_switch) .mutable_ready() .build(), glib::ParamSpecBoolean::builder(PROP_STOP_ON_EOS) .nick("stop on EOS") .blurb("Stop forwarding buffers as soon as one input pad is eos") .default_value(Settings::default().stop_on_eos) .mutable_ready() .build(), ] }); PROPERTIES.as_ref() } fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { match pspec.name() { PROP_ACTIVE_PAD => { let settings = self.settings.lock(); if settings.auto_switch { gst::warning!( CAT, imp = self, "active-pad property setting ignored, because auto-switch=true" ); } else { let active_pad = value .get::>() .expect("type checked upstream"); /* Trigger a pad switch if needed */ if let Some(active_pad) = active_pad { self.set_active_pad( &mut self.state.lock(), active_pad .downcast_ref::() .unwrap(), ); } } drop(settings); } PROP_TIMEOUT => { let mut settings = self.settings.lock(); let new_value = value.get().expect("type checked upstream"); settings.timeout = new_value; debug!(CAT, imp = self, "Timeout now {}", settings.timeout); drop(settings); let _ = self .obj() .post_message(gst::message::Latency::builder().src(&*self.obj()).build()); } PROP_LATENCY => { let mut settings = self.settings.lock(); let new_value = value.get().expect("type checked upstream"); settings.latency = new_value; drop(settings); let _ = self .obj() .post_message(gst::message::Latency::builder().src(&*self.obj()).build()); } PROP_MIN_UPSTREAM_LATENCY => { let mut settings = self.settings.lock(); let new_value = value.get().expect("type checked upstream"); settings.min_upstream_latency = new_value; drop(settings); let _ = self .obj() .post_message(gst::message::Latency::builder().src(&*self.obj()).build()); } PROP_IMMEDIATE_FALLBACK => { let mut settings = self.settings.lock(); let new_value = value.get().expect("type checked upstream"); settings.immediate_fallback = new_value; } PROP_AUTO_SWITCH => { let mut settings = self.settings.lock(); let new_value = value.get().expect("type checked upstream"); settings.auto_switch = new_value; } PROP_STOP_ON_EOS => { let mut settings = self.settings.lock(); let new_value = value.get().expect("type checked upstream"); settings.stop_on_eos = new_value; } _ => unimplemented!(), } } fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { match pspec.name() { PROP_ACTIVE_PAD => { let active_pad = self.active_sinkpad.lock().clone(); active_pad.to_value() } PROP_TIMEOUT => { let settings = self.settings.lock(); settings.timeout.to_value() } PROP_LATENCY => { let settings = self.settings.lock(); settings.latency.to_value() } PROP_MIN_UPSTREAM_LATENCY => { let settings = self.settings.lock(); settings.min_upstream_latency.to_value() } PROP_IMMEDIATE_FALLBACK => { let settings = self.settings.lock(); settings.immediate_fallback.to_value() } PROP_AUTO_SWITCH => { let settings = self.settings.lock(); settings.auto_switch.to_value() } PROP_STOP_ON_EOS => { let settings = self.settings.lock(); settings.stop_on_eos.to_value() } _ => unimplemented!(), } } fn constructed(&self) { self.parent_constructed(); let obj = self.obj(); obj.add_pad(&self.src_pad).unwrap(); obj.set_element_flags(gst::ElementFlags::REQUIRE_CLOCK); } } impl ElementImpl for FallbackSwitch { fn metadata() -> Option<&'static gst::subclass::ElementMetadata> { static ELEMENT_METADATA: LazyLock = LazyLock::new(|| { gst::subclass::ElementMetadata::new( "Priority-based input selector", "Generic", "Priority-based automatic input selector element", "Jan Schmidt ", ) }); Some(&*ELEMENT_METADATA) } fn pad_templates() -> &'static [gst::PadTemplate] { static PAD_TEMPLATES: LazyLock> = LazyLock::new(|| { let caps = gst::Caps::new_any(); let sink_pad_template = gst::PadTemplate::with_gtype( "sink_%u", gst::PadDirection::Sink, gst::PadPresence::Request, &caps, super::FallbackSwitchSinkPad::static_type(), ) .unwrap(); let src_pad_template = gst::PadTemplate::new( "src", gst::PadDirection::Src, gst::PadPresence::Always, &caps, ) .unwrap(); vec![sink_pad_template, src_pad_template] }); PAD_TEMPLATES.as_ref() } fn change_state( &self, transition: gst::StateChange, ) -> Result { trace!(CAT, imp = self, "Changing state {:?}", transition); match transition { gst::StateChange::PlayingToPaused => { self.cancel_waits(); } gst::StateChange::ReadyToNull => { self.reset(); } gst::StateChange::ReadyToPaused => { let mut state = self.state.lock(); let prev_active_pad = self.active_sinkpad.lock().take(); *state = State::default(); let pads = self.obj().sink_pads(); if let Some(pad) = pads.first() { let pad = pad.downcast_ref::().unwrap(); *self.active_sinkpad.lock() = Some(pad.clone()); state.switched_pad = true; state.discont_pending = true; drop(state); if prev_active_pad.as_ref() != Some(pad) { self.obj().notify(PROP_ACTIVE_PAD); } } for pad in pads { let pad = pad.downcast_ref::().unwrap(); let pad_imp = pad.imp(); *pad_imp.state.lock() = SinkState::default(); } } _ => (), } let mut success = self.parent_change_state(transition)?; match transition { gst::StateChange::ReadyToPaused => { success = gst::StateChangeSuccess::NoPreroll; } gst::StateChange::PlayingToPaused => { success = gst::StateChangeSuccess::NoPreroll; } gst::StateChange::PausedToReady => { *self.state.lock() = State::default(); for pad in self.obj().sink_pads() { let pad = pad.downcast_ref::().unwrap(); let pad_imp = pad.imp(); *pad_imp.state.lock() = SinkState::default(); } } _ => (), } Ok(success) } fn request_new_pad( &self, templ: &gst::PadTemplate, _name: Option<&str>, _caps: Option<&gst::Caps>, ) -> Option { let mut state = self.state.lock(); let pad_serial = self.sink_pad_serial.fetch_add(1, Ordering::SeqCst); let pad = gst::PadBuilder::::from_template(templ) .name(format!("sink_{pad_serial}").as_str()) .chain_function(|pad, parent, buffer| { FallbackSwitch::catch_panic_pad_function( parent, || Err(gst::FlowError::Error), |fallbackswitch| fallbackswitch.sink_chain(pad, buffer), ) }) .chain_list_function(|pad, parent, bufferlist| { FallbackSwitch::catch_panic_pad_function( parent, || Err(gst::FlowError::Error), |fallbackswitch| fallbackswitch.sink_chain_list(pad, bufferlist), ) }) .event_function(|pad, parent, event| { FallbackSwitch::catch_panic_pad_function( parent, || false, |fallbackswitch| fallbackswitch.sink_event(pad, event), ) }) .query_function(|pad, parent, query| { FallbackSwitch::catch_panic_pad_function( parent, || false, |fallbackswitch| fallbackswitch.sink_query(pad, query), ) }) .activatemode_function(|pad, _parent, mode, activate| { Self::sink_activatemode(pad, mode, activate) }) .build(); pad.set_active(true).unwrap(); self.obj().add_pad(&pad).unwrap(); let notify_active_pad = match &mut *self.active_sinkpad.lock() { active_sinkpad @ None => { *active_sinkpad = Some(pad.clone()); state.switched_pad = true; state.discont_pending = true; true } _ => false, }; let mut pad_settings = pad.imp().settings.lock(); pad_settings.priority = pad_serial; drop(pad_settings); drop(state); if notify_active_pad { self.obj().notify(PROP_ACTIVE_PAD); } let _ = self .obj() .post_message(gst::message::Latency::builder().src(&*self.obj()).build()); self.obj().child_added(&pad, &pad.name()); Some(pad.upcast()) } fn release_pad(&self, pad: &gst::Pad) { let pad = pad.downcast_ref::().unwrap(); let mut pad_state = pad.imp().state.lock(); pad_state.flush_start(); drop(pad_state); let _ = pad.set_active(false); self.obj().remove_pad(pad).unwrap(); self.obj().child_removed(pad, &pad.name()); let _ = self .obj() .post_message(gst::message::Latency::builder().src(&*self.obj()).build()); } } // Implementation of gst::ChildProxy virtual methods. // // This allows accessing the pads and their properties from e.g. gst-launch. impl ChildProxyImpl for FallbackSwitch { fn children_count(&self) -> u32 { let object = self.obj(); object.num_pads() as u32 } fn child_by_name(&self, name: &str) -> Option { let object = self.obj(); object .pads() .into_iter() .find(|p| p.name() == name) .map(|p| p.upcast()) } fn child_by_index(&self, index: u32) -> Option { let object = self.obj(); object .pads() .into_iter() .nth(index as usize) .map(|p| p.upcast()) } } gst-plugin-fallbackswitch-0.14.3/src/fallbackswitch/mod.rs000064400000000000000000000026611046102023000216630ustar 00000000000000// Copyright (C) 2020 Sebastian Dröge // Copyright (C) 2021 Jan Schmidt // Copyright (C) 2020 Mathieu Duponchelle // Copyright (C) 2022 Vivia Nikolaidou // // This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. // If a copy of the MPL was not distributed with this file, You can obtain one at // . // // SPDX-License-Identifier: MPL-2.0 use gst::glib; use gst::prelude::*; mod imp; // The public Rust wrapper type for our element glib::wrapper! { pub struct FallbackSwitch(ObjectSubclass) @extends gst::Element, gst::Object, @implements gst::ChildProxy; } // The public Rust wrapper type for our sink pad glib::wrapper! { pub struct FallbackSwitchSinkPad(ObjectSubclass) @extends gst::Pad, gst::Object; } // Registers the type for our element, and then registers in GStreamer under // the name "fallbackswitch" for being able to instantiate it via e.g. // gst::ElementFactory::make(). pub fn register(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { #[cfg(feature = "doc")] FallbackSwitchSinkPad::static_type().mark_as_plugin_api(gst::PluginAPIFlags::empty()); gst::Element::register( Some(plugin), "fallbackswitch", gst::Rank::NONE, FallbackSwitch::static_type(), ) } gst-plugin-fallbackswitch-0.14.3/src/lib.rs000064400000000000000000000020621046102023000166640ustar 00000000000000// Copyright (C) 2019 Sebastian Dröge // // This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. // If a copy of the MPL was not distributed with this file, You can obtain one at // . // // SPDX-License-Identifier: MPL-2.0 #![allow(clippy::non_send_fields_in_send_ty, unused_doc_comments)] /** * plugin-fallbackswitch: * * Since: plugins-rs-0.6.0 */ use gst::glib; mod fallbacksrc; mod fallbackswitch; pub use fallbacksrc::{RetryReason, Status}; fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { fallbacksrc::register(plugin)?; fallbackswitch::register(plugin)?; Ok(()) } gst::plugin_define!( fallbackswitch, env!("CARGO_PKG_DESCRIPTION"), plugin_init, concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")), // FIXME: MPL-2.0 is only allowed since 1.18.3 (as unknown) and 1.20 (as known) "MPL", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_NAME"), env!("CARGO_PKG_REPOSITORY"), env!("BUILD_REL_DATE") ); gst-plugin-fallbackswitch-0.14.3/tests/fallbackswitch.rs000064400000000000000000000520211046102023000214520ustar 00000000000000// Copyright (C) 2019 Sebastian Dröge // Copyright (C) 2021 Jan Schmidt // // This Source Code Form is subject to the terms of the Mozilla Public License, v2.0. // If a copy of the MPL was not distributed with this file, You can obtain one at // . // // SPDX-License-Identifier: MPL-2.0 use gst::debug; use gst::prelude::*; use std::sync::LazyLock; const LATENCY: gst::ClockTime = gst::ClockTime::from_mseconds(10); static TEST_CAT: LazyLock = LazyLock::new(|| { gst::DebugCategory::new( "fallbackswitch-test", gst::DebugColorFlags::empty(), Some("fallbackswitch test"), ) }); fn init() { use std::sync::Once; static INIT: Once = Once::new(); INIT.call_once(|| { gst::init().unwrap(); gstfallbackswitch::plugin_register_static().expect("gstfallbackswitch test"); }); } macro_rules! assert_fallback_buffer { ($buffer:expr, $ts:expr) => { assert_eq!($buffer.pts(), $ts); assert_eq!($buffer.size(), 160 * 120 * 4); }; } macro_rules! assert_buffer { ($buffer:expr, $ts:expr) => { assert_eq!($buffer.pts(), $ts); assert_eq!($buffer.size(), 320 * 240 * 4); }; } #[test] fn test_no_fallback_no_drops() { let pipeline = setup_pipeline(None, None, None); push_buffer(&pipeline, gst::ClockTime::ZERO); set_time(&pipeline, gst::ClockTime::ZERO + LATENCY); let buffer = pull_buffer(&pipeline); assert_buffer!(buffer, Some(gst::ClockTime::ZERO)); push_buffer(&pipeline, 1.seconds()); set_time(&pipeline, 1.seconds() + LATENCY); let buffer = pull_buffer(&pipeline); assert_buffer!(buffer, Some(gst::ClockTime::SECOND)); push_buffer(&pipeline, 2.seconds()); set_time(&pipeline, 2.seconds() + LATENCY); let buffer = pull_buffer(&pipeline); assert_buffer!(buffer, Some(2.seconds())); push_eos(&pipeline); wait_eos(&pipeline); stop_pipeline(pipeline); } #[test] fn test_no_drops_live() { test_no_drops(true); } #[test] fn test_no_drops_not_live() { test_no_drops(false); } fn test_no_drops(live: bool) { let pipeline = setup_pipeline(Some(live), None, None); push_buffer(&pipeline, gst::ClockTime::ZERO); push_fallback_buffer(&pipeline, gst::ClockTime::ZERO); set_time(&pipeline, gst::ClockTime::ZERO + LATENCY); let buffer = pull_buffer(&pipeline); assert_buffer!(buffer, Some(gst::ClockTime::ZERO)); push_fallback_buffer(&pipeline, 1.seconds()); push_buffer(&pipeline, 1.seconds()); set_time(&pipeline, 1.seconds() + LATENCY); let buffer = pull_buffer(&pipeline); assert_buffer!(buffer, Some(gst::ClockTime::SECOND)); push_buffer(&pipeline, 2.seconds()); push_fallback_buffer(&pipeline, 2.seconds()); set_time(&pipeline, 2.seconds() + LATENCY); let buffer = pull_buffer(&pipeline); assert_buffer!(buffer, Some(2.seconds())); // EOS on the fallback should not be required push_eos(&pipeline); wait_eos(&pipeline); stop_pipeline(pipeline); } #[test] fn test_no_drops_but_no_fallback_frames_live() { test_no_drops_but_no_fallback_frames(true); } #[test] fn test_no_drops_but_no_fallback_frames_not_live() { test_no_drops_but_no_fallback_frames(false); } fn test_no_drops_but_no_fallback_frames(live: bool) { let pipeline = setup_pipeline(Some(live), None, None); push_buffer(&pipeline, gst::ClockTime::ZERO); // +10ms needed here because the immediate timeout will be always at running time 0, but // aggregator also adds the latency to it so we end up at 10ms instead. set_time(&pipeline, LATENCY); let buffer = pull_buffer(&pipeline); assert_buffer!(buffer, Some(gst::ClockTime::ZERO)); push_buffer(&pipeline, 1.seconds()); set_time(&pipeline, 1.seconds() + LATENCY); let buffer = pull_buffer(&pipeline); assert_buffer!(buffer, Some(gst::ClockTime::SECOND)); push_buffer(&pipeline, 2.seconds()); set_time(&pipeline, 2.seconds() + LATENCY); let buffer = pull_buffer(&pipeline); assert_buffer!(buffer, Some(2.seconds())); // EOS on the fallback should not be required push_eos(&pipeline); wait_eos(&pipeline); stop_pipeline(pipeline); } #[test] fn test_short_drop_live() { test_short_drop(true); } #[test] fn test_short_drop_not_live() { test_short_drop(false); } fn test_short_drop(live: bool) { let pipeline = setup_pipeline(Some(live), None, None); push_buffer(&pipeline, gst::ClockTime::ZERO); push_fallback_buffer(&pipeline, gst::ClockTime::ZERO); set_time(&pipeline, gst::ClockTime::ZERO + LATENCY); let buffer = pull_buffer(&pipeline); assert_buffer!(buffer, Some(gst::ClockTime::ZERO)); // A timeout at 1s will get rid of the fallback buffer // but not output anything push_fallback_buffer(&pipeline, 1.seconds()); // Time out the fallback buffer at +10ms set_time(&pipeline, 1.seconds() + 10.mseconds()); push_fallback_buffer(&pipeline, 2.seconds()); push_buffer(&pipeline, 2.seconds()); set_time(&pipeline, 2.seconds() + LATENCY); let buffer = pull_buffer(&pipeline); assert_buffer!(buffer, Some(2.seconds())); push_eos(&pipeline); push_fallback_eos(&pipeline); wait_eos(&pipeline); stop_pipeline(pipeline); } #[test] fn test_long_drop_and_eos_live() { test_long_drop_and_eos(true); } #[test] fn test_long_drop_and_eos_not_live() { test_long_drop_and_eos(false); } fn test_long_drop_and_eos(live: bool) { let pipeline = setup_pipeline(Some(live), None, None); // Produce the first frame push_buffer(&pipeline, gst::ClockTime::ZERO); push_fallback_buffer(&pipeline, gst::ClockTime::ZERO); set_time(&pipeline, gst::ClockTime::ZERO); let buffer = pull_buffer(&pipeline); assert_buffer!(buffer, Some(gst::ClockTime::ZERO)); // Produce a second frame but only from the fallback source push_fallback_buffer(&pipeline, 1.seconds()); set_time(&pipeline, 1.seconds() + 10.mseconds()); // Produce a third frame but only from the fallback source push_fallback_buffer(&pipeline, 2.seconds()); set_time(&pipeline, 2.seconds() + 10.mseconds()); // Produce a fourth frame but only from the fallback source // This should be output now push_fallback_buffer(&pipeline, 3.seconds()); set_time(&pipeline, 3.seconds() + 10.mseconds()); let buffer = pull_buffer(&pipeline); assert_fallback_buffer!(buffer, Some(3.seconds())); // Produce a fifth frame but only from the fallback source // This should be output now push_fallback_buffer(&pipeline, 4.seconds()); set_time(&pipeline, 4.seconds() + 10.mseconds()); let buffer = pull_buffer(&pipeline); assert_fallback_buffer!(buffer, Some(4.seconds())); // Wait for EOS to arrive at appsink push_eos(&pipeline); push_fallback_eos(&pipeline); wait_eos(&pipeline); stop_pipeline(pipeline); } #[test] fn test_long_drop_and_recover_live() { test_long_drop_and_recover(true); } #[test] fn test_long_drop_and_recover_not_live() { test_long_drop_and_recover(false); } fn test_long_drop_and_recover(live: bool) { let pipeline = setup_pipeline(Some(live), None, None); let switch = pipeline.by_name("switch").unwrap(); let mainsink = switch.static_pad("sink_0").unwrap(); // Produce the first frame push_buffer(&pipeline, gst::ClockTime::ZERO); push_fallback_buffer(&pipeline, gst::ClockTime::ZERO); set_time(&pipeline, gst::ClockTime::ZERO); let buffer = pull_buffer(&pipeline); assert_buffer!(buffer, Some(gst::ClockTime::ZERO)); assert!(mainsink.property::("is-healthy")); // Produce a second frame but only from the fallback source push_fallback_buffer(&pipeline, 1.seconds()); set_time(&pipeline, 1.seconds() + 10.mseconds()); // Produce a third frame but only from the fallback source push_fallback_buffer(&pipeline, 2.seconds()); set_time(&pipeline, 2.seconds() + 10.mseconds()); // Produce a fourth frame but only from the fallback source // This should be output now push_fallback_buffer(&pipeline, 3.seconds()); set_time(&pipeline, 3.seconds() + 10.mseconds()); let buffer = pull_buffer(&pipeline); assert_fallback_buffer!(buffer, Some(3.seconds())); // Produce a fifth frame but only from the fallback source // This should be output now push_fallback_buffer(&pipeline, 4.seconds()); set_time(&pipeline, 4.seconds() + 10.mseconds()); let buffer = pull_buffer(&pipeline); assert_fallback_buffer!(buffer, Some(4.seconds())); // Produce a sixth frame from the normal source, which // will make it healthy again push_buffer(&pipeline, 5.seconds()); set_time(&pipeline, 5.seconds() + 10.mseconds()); let buffer = pull_buffer(&pipeline); assert_buffer!(buffer, Some(5.seconds())); assert!(mainsink.property::("is-healthy")); drop(mainsink); drop(switch); // Produce a seventh frame from the normal source but no fallback. // This should still be output immediately push_buffer(&pipeline, 6.seconds()); set_time(&pipeline, 6.seconds() + 10.mseconds()); let buffer = pull_buffer(&pipeline); assert_buffer!(buffer, Some(6.seconds())); // Produce a eight frame from the normal source push_buffer(&pipeline, 7.seconds()); push_fallback_buffer(&pipeline, 7.seconds()); set_time(&pipeline, 7.seconds() + 10.mseconds()); let buffer = pull_buffer(&pipeline); assert_buffer!(buffer, Some(7.seconds())); // Wait for EOS to arrive at appsink push_eos(&pipeline); push_fallback_eos(&pipeline); wait_eos(&pipeline); stop_pipeline(pipeline); } #[test] fn test_initial_timeout_live() { test_initial_timeout(true); } #[test] fn test_initial_timeout_not_live() { test_initial_timeout(false); } fn test_initial_timeout(live: bool) { let pipeline = setup_pipeline(Some(live), None, None); // Produce the first frame but only from the fallback source push_fallback_buffer(&pipeline, gst::ClockTime::ZERO); set_time(&pipeline, gst::ClockTime::ZERO); // Produce a second frame but only from the fallback source push_fallback_buffer(&pipeline, 1.seconds()); set_time(&pipeline, 1.seconds() + 10.mseconds()); // Produce a third frame but only from the fallback source push_fallback_buffer(&pipeline, 2.seconds()); set_time(&pipeline, 2.seconds() + 10.mseconds()); // Produce a fourth frame but only from the fallback source // This should be output now push_fallback_buffer(&pipeline, 3.seconds()); set_time(&pipeline, 3.seconds() + 10.mseconds()); let buffer = pull_buffer(&pipeline); assert_fallback_buffer!(buffer, Some(3.seconds())); // Produce a fifth frame but only from the fallback source // This should be output now push_fallback_buffer(&pipeline, 4.seconds()); set_time(&pipeline, 4.seconds() + 10.mseconds()); let buffer = pull_buffer(&pipeline); assert_fallback_buffer!(buffer, Some(4.seconds())); // Wait for EOS to arrive at appsink push_eos(&pipeline); push_fallback_eos(&pipeline); wait_eos(&pipeline); stop_pipeline(pipeline); } #[test] fn test_immediate_fallback_live() { test_immediate_fallback(true); } #[test] fn test_immediate_fallback_not_live() { test_immediate_fallback(false); } fn test_immediate_fallback(live: bool) { let pipeline = setup_pipeline(Some(live), Some(true), None); // Produce the first frame but only from the fallback source push_fallback_buffer(&pipeline, gst::ClockTime::ZERO); set_time(&pipeline, gst::ClockTime::ZERO); let buffer = pull_buffer(&pipeline); assert_fallback_buffer!(buffer, Some(gst::ClockTime::ZERO)); // Wait for EOS to arrive at appsink push_eos(&pipeline); push_fallback_eos(&pipeline); wait_eos(&pipeline); stop_pipeline(pipeline); } #[test] fn test_manual_switch_live() { test_manual_switch(true); } #[test] fn test_manual_switch_not_live() { test_manual_switch(false); } fn test_manual_switch(live: bool) { let pipeline = setup_pipeline(Some(live), None, Some(false)); let switch = pipeline.by_name("switch").unwrap(); let mainsink = switch.static_pad("sink_0").unwrap(); let fallbacksink = switch.static_pad("sink_1").unwrap(); switch.set_property("active-pad", &mainsink); push_buffer(&pipeline, gst::ClockTime::ZERO); push_fallback_buffer(&pipeline, gst::ClockTime::ZERO); set_time(&pipeline, gst::ClockTime::ZERO + LATENCY); let buffer = pull_buffer(&pipeline); assert_buffer!(buffer, Some(gst::ClockTime::ZERO)); switch.set_property("active-pad", &fallbacksink); push_fallback_buffer(&pipeline, 1.seconds()); push_buffer(&pipeline, 1.seconds()); set_time(&pipeline, 1.seconds() + LATENCY); let mut buffer = pull_buffer(&pipeline); // FIXME: Sometimes we first get the ZERO buffer from the fallback sink if buffer.pts() == Some(gst::ClockTime::ZERO) { buffer = pull_buffer(&pipeline); } assert_fallback_buffer!(buffer, Some(gst::ClockTime::SECOND)); switch.set_property("active-pad", &mainsink); push_buffer(&pipeline, 2.seconds()); push_fallback_buffer(&pipeline, 2.seconds()); set_time(&pipeline, 2.seconds() + LATENCY); buffer = pull_buffer(&pipeline); // FIXME: Sometimes we first get the 1sec buffer from the main sink if buffer.pts() == Some(gst::ClockTime::SECOND) { buffer = pull_buffer(&pipeline); } assert_buffer!(buffer, Some(2.seconds())); drop(mainsink); drop(fallbacksink); drop(switch); // EOS on the fallback should not be required push_eos(&pipeline); wait_eos(&pipeline); stop_pipeline(pipeline); } struct Pipeline { pipeline: gst::Pipeline, clock_join_handle: Option>, } impl std::ops::Deref for Pipeline { type Target = gst::Pipeline; fn deref(&self) -> &gst::Pipeline { &self.pipeline } } fn setup_pipeline( with_live_fallback: Option, immediate_fallback: Option, auto_switch: Option, ) -> Pipeline { init(); debug!(TEST_CAT, "Setting up pipeline"); let clock = gst_check::TestClock::new(); clock.set_time(gst::ClockTime::ZERO); let pipeline = gst::Pipeline::default(); // Running time 0 in our pipeline is going to be clock time 1s. All // clock ids before 1s are used for signalling to our clock advancing // thread. pipeline.use_clock(Some(&clock)); pipeline.set_base_time(gst::ClockTime::SECOND); pipeline.set_start_time(gst::ClockTime::NONE); let src = gst_app::AppSrc::builder() .name("src") .is_live(true) .format(gst::Format::Time) .min_latency(LATENCY.nseconds() as i64) .caps( &gst_video::VideoCapsBuilder::new() .format(gst_video::VideoFormat::Argb) .width(320) .height(240) .framerate((0, 1).into()) .build(), ) .build(); let switch = gst::ElementFactory::make("fallbackswitch") .name("switch") .property("timeout", 3.seconds()) .property_if_some("immediate-fallback", immediate_fallback) .property_if_some("auto-switch", auto_switch) .build() .unwrap(); let sink = gst_app::AppSink::builder().name("sink").sync(false).build(); let queue = gst::ElementFactory::make("queue").build().unwrap(); pipeline .add_many([src.upcast_ref(), &switch, &queue, sink.upcast_ref()]) .unwrap(); src.link_pads(Some("src"), &switch, Some("sink_0")).unwrap(); switch.link_pads(Some("src"), &queue, Some("sink")).unwrap(); queue.link_pads(Some("src"), &sink, Some("sink")).unwrap(); let sink_pad = switch.static_pad("sink_0").unwrap(); sink_pad.set_property("priority", 0u32); if let Some(live) = with_live_fallback { let fallback_src = gst_app::AppSrc::builder() .name("fallback-src") .is_live(live) .format(gst::Format::Time) .min_latency(LATENCY.nseconds() as i64) .caps( &gst_video::VideoCapsBuilder::new() .format(gst_video::VideoFormat::Argb) .width(160) .height(120) .framerate((0, 1).into()) .build(), ) .build(); pipeline.add(&fallback_src).unwrap(); fallback_src .link_pads(Some("src"), &switch, Some("sink_1")) .unwrap(); let sink_pad = switch.static_pad("sink_1").unwrap(); sink_pad.set_property("priority", 1u32); } pipeline.set_state(gst::State::Playing).unwrap(); let clock_join_handle = std::thread::spawn(move || { loop { while let Some(clock_id) = clock.peek_next_pending_id().and_then(|clock_id| { // Process if the clock ID is in the past or now if clock.time() >= clock_id.time() { Some(clock_id) } else { None } }) { debug!( TEST_CAT, "Processing clock ID {} at {:?}", clock_id.time(), clock.time() ); if let Some(clock_id) = clock.process_next_clock_id() { debug!(TEST_CAT, "Processed clock ID {}", clock_id.time()); if clock_id.time().is_zero() { debug!(TEST_CAT, "Stopping clock thread"); return; } } } // Sleep for 5ms as long as we have pending clock IDs that are in the future // at the top of the queue. We don't want to do a busy loop here. while clock.peek_next_pending_id().iter().any(|clock_id| { // Sleep if the clock ID is in the future clock.time() < clock_id.time() }) { use std::{thread, time}; thread::sleep(time::Duration::from_millis(10)); } // Otherwise if there are none (or they are ready now) wait until there are // clock ids again let _ = clock.wait_for_next_pending_id(); } }); Pipeline { pipeline, clock_join_handle: Some(clock_join_handle), } } fn push_buffer(pipeline: &Pipeline, time: gst::ClockTime) { let src = pipeline .by_name("src") .unwrap() .downcast::() .unwrap(); let mut buffer = gst::Buffer::with_size(320 * 240 * 4).unwrap(); { let buffer = buffer.get_mut().unwrap(); buffer.set_pts(time); } src.push_buffer(buffer).unwrap(); } fn push_fallback_buffer(pipeline: &Pipeline, time: gst::ClockTime) { let src = pipeline .by_name("fallback-src") .unwrap() .downcast::() .unwrap(); let mut buffer = gst::Buffer::with_size(160 * 120 * 4).unwrap(); { let buffer = buffer.get_mut().unwrap(); buffer.set_pts(time); } src.push_buffer(buffer).unwrap(); } fn push_eos(pipeline: &Pipeline) { let src = pipeline .by_name("src") .unwrap() .downcast::() .unwrap(); src.end_of_stream().unwrap(); } fn push_fallback_eos(pipeline: &Pipeline) { let src = pipeline .by_name("fallback-src") .unwrap() .downcast::() .unwrap(); src.end_of_stream().unwrap(); } fn pull_buffer(pipeline: &Pipeline) -> gst::Buffer { let sink = pipeline .by_name("sink") .unwrap() .downcast::() .unwrap(); let sample = sink.pull_sample().unwrap(); sample.buffer_owned().unwrap() } fn set_time(pipeline: &Pipeline, time: gst::ClockTime) { let clock = pipeline .clock() .unwrap() .downcast::() .unwrap(); debug!(TEST_CAT, "Setting time to {}", time); clock.set_time(gst::ClockTime::SECOND + time); } fn wait_eos(pipeline: &Pipeline) { let sink = pipeline .by_name("sink") .unwrap() .downcast::() .unwrap(); // FIXME: Ideally without a sleep loop { use std::{thread, time}; if sink.is_eos() { debug!(TEST_CAT, "Waited for EOS"); break; } thread::sleep(time::Duration::from_millis(10)); } } fn stop_pipeline(mut pipeline: Pipeline) { pipeline.set_state(gst::State::Null).unwrap(); let clock = pipeline .clock() .unwrap() .downcast::() .unwrap(); // Signal shutdown to the clock thread let clock_id = clock.new_single_shot_id(gst::ClockTime::ZERO); let _ = clock_id.wait(); let switch = pipeline.by_name("switch").unwrap(); let switch_weak = switch.downgrade(); drop(switch); let pipeline_weak = pipeline.downgrade(); pipeline.clock_join_handle.take().unwrap().join().unwrap(); drop(pipeline); assert!(switch_weak.upgrade().is_none()); assert!(pipeline_weak.upgrade().is_none()); }