diff options
-rw-r--r-- | Cargo.lock | 96 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | etc/taskcluster/decision_task.py | 9 | ||||
-rw-r--r-- | ports/gstplugin/Cargo.toml | 38 | ||||
-rw-r--r-- | ports/gstplugin/README.md | 131 | ||||
-rw-r--r-- | ports/gstplugin/build.rs | 7 | ||||
-rw-r--r-- | ports/gstplugin/lib.rs | 40 | ||||
-rw-r--r-- | ports/gstplugin/logging.rs | 43 | ||||
-rw-r--r-- | ports/gstplugin/resources.rs | 92 | ||||
-rw-r--r-- | ports/gstplugin/servosrc.rs | 618 | ||||
-rw-r--r-- | ports/gstplugin/test.html | 16 | ||||
-rw-r--r-- | python/servo/bootstrap.py | 2 | ||||
-rw-r--r-- | python/servo/command_base.py | 6 |
13 files changed, 1088 insertions, 11 deletions
diff --git a/Cargo.lock b/Cargo.lock index a501e46e194..6da32ec03dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -581,10 +581,11 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.2" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cce36c92cb605414e9b824f866f5babe0a0368e39ea07393b9b63cf3844c0e6" +checksum = "e8493056968583b0193c1bb04d6f7684586f3726992d6c573261941a895dbd68" dependencies = [ + "libc", "num-integer", "num-traits", "time", @@ -1754,6 +1755,21 @@ dependencies = [ ] [[package]] +name = "git2" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb400360e8a4d61b10e648285bbfa919bbf9519d0d5d5720354456f44349226" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + +[[package]] name = "gl_generator" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1916,6 +1932,17 @@ dependencies = [ ] [[package]] +name = "gst-plugin-version-helper" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90db297b7d445b643b411be6c43e33b78670d0579bec256aeff644b693b0eccb" +dependencies = [ + "chrono", + "git2", + "toml", +] + +[[package]] name = "gstreamer" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2820,9 +2847,9 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lazycell" @@ -2873,6 +2900,20 @@ dependencies = [ ] [[package]] +name = "libgit2-sys" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c179ed6d19cd3a051e68c177fbbc214e79ac4724fac3a850ec9f3d3eb8a5578" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] name = "libloading" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2956,6 +2997,20 @@ dependencies = [ ] [[package]] +name = "libssh2-sys" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fcd5a428a31cbbfe059812d74f4b6cd3b9b7426c2bdaec56993c5365da1c328" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] name = "libz-sys" version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3643,6 +3698,12 @@ dependencies = [ ] [[package]] +name = "openssl-probe" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" + +[[package]] name = "openssl-sys" version = "0.9.35" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4715,6 +4776,29 @@ dependencies = [ ] [[package]] +name = "servo-gst-plugin" +version = "0.0.1" +dependencies = [ + "crossbeam-channel", + "euclid", + "gleam 0.6.18", + "glib", + "gst-plugin-version-helper", + "gstreamer", + "gstreamer-base", + "gstreamer-gl", + "gstreamer-video", + "lazy_static", + "libservo", + "log", + "servo-media", + "sparkle", + "surfman", + "surfman-chains", + "surfman-chains-api", +] + +[[package]] name = "servo-media" version = "0.1.0" source = "git+https://github.com/servo/media#220ed1388f2ba008b05f5e94aca21dd14aa37290" @@ -5389,9 +5473,9 @@ dependencies = [ [[package]] name = "surfman-chains" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b0d399c15d8f4aad59cd98edbf58e4c96a3b711cf8daf1b006acab8aec97cb" +checksum = "a2c1b5976b229a807a9e79b3b5248da577948b9882c77f2afce27cf562f80e22" dependencies = [ "euclid", "fnv", diff --git a/Cargo.toml b/Cargo.toml index 3bb824b3417..96706387e0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "ports/glutin", + "ports/gstplugin", "ports/libsimpleservo/capi/", "ports/libsimpleservo/jniapi/", "ports/libmlservo/", diff --git a/etc/taskcluster/decision_task.py b/etc/taskcluster/decision_task.py index 9342e658830..6abdd0d7e11 100644 --- a/etc/taskcluster/decision_task.py +++ b/etc/taskcluster/decision_task.py @@ -184,6 +184,7 @@ def linux_tidy_unit_untrusted(): .with_script(""" ./mach test-tidy --no-progress --all ./mach test-tidy --no-progress --self-test + ./mach bootstrap-gstreamer ./mach build --dev ./mach test-unit @@ -207,6 +208,7 @@ def linux_tidy_unit(): ./mach build --dev --features canvas2d-raqote ./mach build --dev --features layout-2020 ./mach build --dev --libsimpleservo + ./mach build --dev -p servo-gst-plugin ./mach test-tidy --no-progress --self-test ./etc/memory_reports_over_time.py --test @@ -485,6 +487,8 @@ def windows_unit(cached=True): "mach smoketest --angle", "mach package --dev", "mach build --dev --libsimpleservo", + "mach build --dev -p servo-gst-plugin", + ) .with_artifacts("repo/target/debug/msi/Servo.exe", "repo/target/debug/msi/Servo.zip") @@ -814,7 +818,10 @@ def linux_build_task(name, *, build_env=build_env, install_rustc_dev=True): .with_dockerfile(dockerfile_path("build")) .with_env(**build_env, **unix_build_env, **linux_build_env) .with_repo_bundle() - .with_script("rustup set profile minimal") + .with_script(""" + rustup set profile minimal + ./mach bootstrap-gstreamer + """) ) if install_rustc_dev: # required by components/script_plugins: diff --git a/ports/gstplugin/Cargo.toml b/ports/gstplugin/Cargo.toml new file mode 100644 index 00000000000..cb72e6b96f8 --- /dev/null +++ b/ports/gstplugin/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "servo-gst-plugin" +description = "A GStreamer plugin that provides servosrc" +version = "0.0.1" +authors = ["The Servo Project Developers"] +license = "MPL-2.0" +edition = "2018" +build = "build.rs" +repository = "https://github.com/servo/servo/" +publish = false + +[lib] +name = "gstservoplugin" +crate-type = ["cdylib"] +path = "lib.rs" + +[dependencies] +crossbeam-channel = "0.3" +euclid = "0.20" +gleam = "0.6" +glib = { version = "0.8", features = ["subclassing"] } +gstreamer = { version = "0.14", features = ["subclassing"] } +gstreamer-base = { version = "0.14", features = ["subclassing"] } +gstreamer-gl = { version = "0.14", features = ["v1_16"] } +gstreamer-video = { version = "0.14", features = ["subclassing"] } +log = "0.4" +lazy_static = "1.4" +libservo = {path = "../../components/servo"} +servo-media = {git = "https://github.com/servo/media"} +sparkle = "0.1" +# NOTE: the sm-angle-default feature only enables angle on windows, not other platforms! +surfman = { version = "0.1", features = ["sm-angle-default", "sm-osmesa"] } +surfman-chains-api = "0.2" +surfman-chains = "0.2.1" + +[build-dependencies] +gst-plugin-version-helper = "0.1" + diff --git a/ports/gstplugin/README.md b/ports/gstplugin/README.md new file mode 100644 index 00000000000..6c0a5d5095b --- /dev/null +++ b/ports/gstplugin/README.md @@ -0,0 +1,131 @@ +# A GStreamer plugin which runs servo + +## Build + +``` +./mach build -r -p servo-gst-plugin +``` + +## Install + +By default, gstreamer's plugin finder will complain about any libraries it finds that aren't +gstreamer plugins, so we need to have a directory just for plugins: +``` +mkdir target/gstplugins +``` + +To install: +``` +cp target/release/libgstservoplugin.* target/gstplugins +``` +## Run + +To run locally: +``` +GST_PLUGIN_PATH=target/gstplugins \ + gst-launch-1.0 servosrc \ + ! queue \ + ! video/x-raw,framerate=25/1,width=512,height=512 \ + ! videoflip video-direction=vert \ + ! autovideosink +``` + +To stream over the network: +``` +GST_PLUGIN_PATH=target/gstplugins \ + gst-launch-1.0 servosrc \ + ! queue \ + ! video/x-raw,framerate=25/1,width=512,height=512 \ + ! videoconvert \ + ! videoflip video-direction=vert \ + ! theoraenc \ + ! oggmux \ + ! tcpserversink host=127.0.0.1 port=8080 +``` + +To save to a file: +``` +GST_PLUGIN_PATH=target/gstplugins \ + gst-launch-1.0 servosrc num-buffers=2000 \ + ! queue \ + ! video/x-raw,framerate=25/1,width=512,height=512 \ + ! videoconvert \ + ! videoflip video-direction=vert \ + ! theoraenc \ + ! oggmux \ + ! filesink location=test.ogg +``` + +*Note*: killing the gstreamer pipeline with control-C sometimes locks up macOS to the point +of needing a power cycle. Killing the pipeline by closing the window seems to work. + +## Troubleshooting building the plugin + +You may need to make sure rust picks up the right gstreamer, for example: +``` +PKG_CONFIG_PATH=$PWD/support/linux/gstreamer/gst/lib \ +LD_LIBRARY_PATH=$PWD/support/linux/gstreamer/gst/lib \ + ./mach build -r -p servo-gst-plugin +``` + +## Troubleshooting running the plugin + +*Currently x11 support is broken!* + +First try: +``` +GST_PLUGIN_PATH=target/gstplugins \ + gst-inspect-1.0 servosrc +``` + +If that doesn't work, try: +``` +GST_PLUGIN_PATH=target/gstplugins \ + gst-in2spect-1.0 target/gstplugins/libgstservoplugin.so +``` + +If you get reports about the plugin being blacklisted, remove the (global!) gstreamer cache, e.g. under Linux: +``` +rm -r ~/.cache/gstreamer-1.0 +``` + +If you get complaints about not being able to find libraries, set `LD_LIBRARY_PATH`, e.g. to use Servo's Linux gstreamer: +``` +LD_LIBRARY_PATH=$PWD/support/linux/gstreamer/gst/lib +``` + +If you get complaints `cannot allocate memory in static TLS block` this is caused by gstreamer initializing threads using +the system alloc, which causes problems if those threads run Rust code that uses jemalloc. The fix is to preload the plugin: +``` +LD_PRELOAD=$PWD/target/gstplugins/libgstservoplugin.so +``` + +You may need to set `GST_PLUGIN_SCANNER`, e.g. to use Servo's: +``` +GST_PLUGIN_SCANNER=$PWD/support/linux/gstreamer/gst/libexec/gstreamer-1.0/gst-plugin-scanner +``` + +You may need to include other directories on the plugin search path, e.g. Servo's gstreamer: +``` +GST_PLUGIN_PATH=$PWD/target/gstplugins/:$PWD/support/linux/gstreamer/gst/lib +``` + +Under X11 you may get complaints about X11 threads not being initialized: +``` +GST_GL_XINITTHREADS=1 +``` + +Under x11 you may get a frozen display from `autovideosink`, try `ximagesink` instead. + +Putting that all together: +``` +GST_GL_XINITTHREADS=1 \ +GST_PLUGIN_PATH=$PWD/target/gstplugins/:$PWD/support/linux/gstreamer/gst/lib \ +GST_PLUGIN_SCANNER=$PWD/support/linux/gstreamer/gst/libexec/gstreamer-1.0/gst-plugin-scanner \ +LD_LIBRARY_PATH=$PWD/support/linux/gstreamer/gst/lib \ +LD_PRELOAD=$PWD/target/gstplugins/libgstservoplugin.so \ + gst-launch-1.0 servosrc \ + ! queue \ + ! videoflip video-direction=vert \ + ! ximagesink +``` diff --git a/ports/gstplugin/build.rs b/ports/gstplugin/build.rs new file mode 100644 index 00000000000..d1555ca6b37 --- /dev/null +++ b/ports/gstplugin/build.rs @@ -0,0 +1,7 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +fn main() { + gst_plugin_version_helper::get_info() +} diff --git a/ports/gstplugin/lib.rs b/ports/gstplugin/lib.rs new file mode 100644 index 00000000000..af8e384cd1c --- /dev/null +++ b/ports/gstplugin/lib.rs @@ -0,0 +1,40 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +use glib::subclass::types::ObjectSubclass; +use gstreamer::gst_plugin_define; +use servosrc::ServoSrc; + +mod logging; +mod resources; +mod servosrc; + +gst_plugin_define!( + servoplugin, + env!("CARGO_PKG_DESCRIPTION"), + plugin_init, + concat!(env!("CARGO_PKG_VERSION"), "-", env!("COMMIT_ID")), + "MPL", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_REPOSITORY"), + env!("BUILD_REL_DATE") +); + +fn plugin_init(plugin: &gstreamer::Plugin) -> Result<(), glib::BoolError> { + gstreamer::gst_debug!(logging::CATEGORY, "Initializing logging"); + log::set_logger(&logging::LOGGER).expect("Failed to set logger"); + log::set_max_level(log::LevelFilter::Debug); + + log::debug!("Initializing resources"); + resources::init(); + + log::debug!("Registering plugin"); + gstreamer::Element::register( + Some(plugin), + "servosrc", + gstreamer::Rank::None, + ServoSrc::get_type(), + ) +} diff --git a/ports/gstplugin/logging.rs b/ports/gstplugin/logging.rs new file mode 100644 index 00000000000..f4838ec015d --- /dev/null +++ b/ports/gstplugin/logging.rs @@ -0,0 +1,43 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +use gstreamer::DebugCategory; +use gstreamer::DebugColorFlags; +use gstreamer::DebugLevel; +use lazy_static::lazy_static; + +lazy_static! { + pub static ref CATEGORY: DebugCategory = + DebugCategory::new("servosrc", DebugColorFlags::empty(), Some("Servo")); +} + +pub static LOGGER: ServoSrcLogger = ServoSrcLogger; + +pub struct ServoSrcLogger; + +impl log::Log for ServoSrcLogger { + fn enabled(&self, _metadata: &log::Metadata) -> bool { + true + } + + fn log(&self, record: &log::Record) { + let lvl = match record.level() { + log::Level::Error => DebugLevel::Error, + log::Level::Warn => DebugLevel::Warning, + log::Level::Info => DebugLevel::Info, + log::Level::Debug => DebugLevel::Debug, + log::Level::Trace => DebugLevel::Trace, + }; + CATEGORY.log::<gstreamer::Object>( + None, + lvl, + record.file().unwrap_or(""), + record.module_path().unwrap_or(""), + record.line().unwrap_or(0), + record.args().clone(), + ); + } + + fn flush(&self) {} +} diff --git a/ports/gstplugin/resources.rs b/ports/gstplugin/resources.rs new file mode 100644 index 00000000000..eba01e72631 --- /dev/null +++ b/ports/gstplugin/resources.rs @@ -0,0 +1,92 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +// This is a copy of the resource loader from the glutin port +// TODO: move this to somewhere where it can be shared. +// https://github.com/servo/servo/issues/24853 + +use lazy_static::lazy_static; +use servo::embedder_traits::resources::{self, Resource}; +use std::env; +use std::fs; +use std::io; +use std::path::PathBuf; +use std::sync::Mutex; + +lazy_static! { + static ref CMD_RESOURCE_DIR: Mutex<Option<String>> = Mutex::new(None); +} + +struct ResourceReader; + +fn filename(file: Resource) -> &'static str { + match file { + Resource::Preferences => "prefs.json", + Resource::BluetoothBlocklist => "gatt_blocklist.txt", + Resource::DomainList => "public_domains.txt", + Resource::HstsPreloadList => "hsts_preload.json", + Resource::SSLCertificates => "certs", + Resource::BadCertHTML => "badcert.html", + Resource::NetErrorHTML => "neterror.html", + Resource::UserAgentCSS => "user-agent.css", + Resource::ServoCSS => "servo.css", + Resource::PresentationalHintsCSS => "presentational-hints.css", + Resource::QuirksModeCSS => "quirks-mode.css", + Resource::RippyPNG => "rippy.png", + Resource::MediaControlsCSS => "media-controls.css", + Resource::MediaControlsJS => "media-controls.js", + } +} + +pub fn init() { + resources::set(Box::new(ResourceReader)); +} + +fn resources_dir_path() -> io::Result<PathBuf> { + // This needs to be called before the process is sandboxed + // as we only give permission to read inside the resources directory, + // not the permissions the "search" for the resources directory. + let mut dir = CMD_RESOURCE_DIR.lock().unwrap(); + if let Some(ref path) = *dir { + return Ok(PathBuf::from(path)); + } + + // FIXME: Find a way to not rely on the executable being + // under `<servo source>[/$target_triple]/target/debug` + // or `<servo source>[/$target_triple]/target/release`. + let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + // Follow symlink + path = path.canonicalize()?; + + while path.pop() { + path.push("resources"); + if path.is_dir() { + break; + } + path.pop(); + // Check for Resources on mac when using a case sensitive filesystem. + path.push("Resources"); + if path.is_dir() { + break; + } + path.pop(); + } + *dir = Some(path.to_str().unwrap().to_owned()); + Ok(path) +} + +impl resources::ResourceReaderMethods for ResourceReader { + fn read(&self, file: Resource) -> Vec<u8> { + let file = filename(file); + let mut path = resources_dir_path().expect("Can't find resources directory"); + path.push(file); + fs::read(path.clone()).expect(&format!("Can't read file {:?}", path)) + } + fn sandbox_access_files_dirs(&self) -> Vec<PathBuf> { + vec![resources_dir_path().expect("Can't find resources directory")] + } + fn sandbox_access_files(&self) -> Vec<PathBuf> { + vec![] + } +} diff --git a/ports/gstplugin/servosrc.rs b/ports/gstplugin/servosrc.rs new file mode 100644 index 00000000000..1698a711031 --- /dev/null +++ b/ports/gstplugin/servosrc.rs @@ -0,0 +1,618 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +use crate::logging::CATEGORY; + +use crossbeam_channel::Receiver; +use crossbeam_channel::Sender; + +use euclid::Point2D; +use euclid::Rect; +use euclid::Scale; +use euclid::Size2D; + +use glib::glib_bool_error; +use glib::glib_object_impl; +use glib::glib_object_subclass; +use glib::object::Cast; +use glib::subclass::object::ObjectImpl; +use glib::subclass::object::ObjectImplExt; +use glib::subclass::simple::ClassStruct; +use glib::subclass::types::ObjectSubclass; +use gstreamer::gst_element_error; +use gstreamer::gst_loggable_error; +use gstreamer::subclass::element::ElementClassSubclassExt; +use gstreamer::subclass::element::ElementImpl; +use gstreamer::subclass::ElementInstanceStruct; +use gstreamer::BufferRef; +use gstreamer::Caps; +use gstreamer::CoreError; +use gstreamer::ErrorMessage; +use gstreamer::FlowError; +use gstreamer::FlowSuccess; +use gstreamer::Format; +use gstreamer::Fraction; +use gstreamer::FractionRange; +use gstreamer::IntRange; +use gstreamer::LoggableError; +use gstreamer::PadDirection; +use gstreamer::PadPresence; +use gstreamer::PadTemplate; +use gstreamer_base::subclass::base_src::BaseSrcImpl; +use gstreamer_base::BaseSrc; +use gstreamer_base::BaseSrcExt; +use gstreamer_video::VideoFormat; +use gstreamer_video::VideoFrameRef; +use gstreamer_video::VideoInfo; + +use log::debug; +use log::info; + +use servo::compositing::windowing::AnimationState; +use servo::compositing::windowing::EmbedderCoordinates; +use servo::compositing::windowing::EmbedderMethods; +use servo::compositing::windowing::WindowEvent; +use servo::compositing::windowing::WindowMethods; +use servo::embedder_traits::EventLoopWaker; +use servo::msg::constellation_msg::TopLevelBrowsingContextId; +use servo::servo_url::ServoUrl; +use servo::webrender_api::units::DevicePixel; +use servo::Servo; + +use sparkle::gl; +use sparkle::gl::types::GLuint; +use sparkle::gl::Gl; + +use surfman::platform::generic::universal::context::Context; +use surfman::platform::generic::universal::device::Device; +use surfman::SurfaceAccess; +use surfman::SurfaceType; + +use surfman_chains::SwapChain; +use surfman_chains_api::SwapChainAPI; + +use std::cell::RefCell; +use std::convert::TryFrom; +use std::rc::Rc; +use std::sync::Mutex; +use std::thread; + +pub struct ServoSrc { + sender: Sender<ServoSrcMsg>, + swap_chain: SwapChain, + info: Mutex<Option<VideoInfo>>, +} + +struct ServoSrcGfx { + device: Device, + context: Context, + gl: Rc<Gl>, + fbo: GLuint, +} + +impl ServoSrcGfx { + fn new() -> ServoSrcGfx { + let version = surfman::GLVersion { major: 4, minor: 3 }; + let flags = surfman::ContextAttributeFlags::empty(); + let attributes = surfman::ContextAttributes { version, flags }; + + let connection = surfman::Connection::new().expect("Failed to create connection"); + let adapter = surfman::Adapter::default().expect("Failed to create adapter"); + let mut device = + surfman::Device::new(&connection, &adapter).expect("Failed to create device"); + let descriptor = device + .create_context_descriptor(&attributes) + .expect("Failed to create descriptor"); + let context = device + .create_context(&descriptor) + .expect("Failed to create context"); + let gl = Gl::gl_fns(gl::ffi_gl::Gl::load_with(|s| { + device.get_proc_address(&context, s) + })); + + // This is a workaround for surfman having a different bootstrap API with Angle + #[cfg(target_os = "windows")] + let mut device = device; + #[cfg(not(target_os = "windows"))] + let mut device = Device::Hardware(device); + #[cfg(target_os = "windows")] + let mut context = context; + #[cfg(not(target_os = "windows"))] + let mut context = Context::Hardware(context); + + device.make_context_current(&context).unwrap(); + + let size = Size2D::new(512, 512); + let surface_type = SurfaceType::Generic { size }; + let surface = device + .create_surface(&mut context, SurfaceAccess::GPUCPU, &surface_type) + .expect("Failed to create surface"); + + gl.viewport(0, 0, size.width, size.height); + debug_assert_eq!(gl.get_error(), gl::NO_ERROR); + + device + .bind_surface_to_context(&mut context, surface) + .expect("Failed to bind surface"); + let fbo = device + .context_surface_info(&context) + .expect("Failed to get context info") + .expect("Failed to get context info") + .framebuffer_object; + gl.bind_framebuffer(gl::FRAMEBUFFER, fbo); + debug_assert_eq!( + (gl.check_framebuffer_status(gl::FRAMEBUFFER), gl.get_error()), + (gl::FRAMEBUFFER_COMPLETE, gl::NO_ERROR) + ); + + let fbo = gl.gen_framebuffers(1)[0]; + debug_assert_eq!( + (gl.check_framebuffer_status(gl::FRAMEBUFFER), gl.get_error()), + (gl::FRAMEBUFFER_COMPLETE, gl::NO_ERROR) + ); + + device.make_no_context_current().unwrap(); + + Self { + device, + context, + gl, + fbo, + } + } +} + +impl Drop for ServoSrcGfx { + fn drop(&mut self) { + let _ = self.device.destroy_context(&mut self.context); + } +} + +thread_local! { + static GFX: RefCell<ServoSrcGfx> = RefCell::new(ServoSrcGfx::new()); +} + +#[derive(Debug)] +enum ServoSrcMsg { + GetSwapChain(Sender<SwapChain>), + Resize(Size2D<i32, DevicePixel>), + Heartbeat, + Quit, +} + +const DEFAULT_URL: &'static str = + "https://rawcdn.githack.com/mrdoob/three.js/r105/examples/webgl_animation_cloth.html"; + +struct ServoThread { + receiver: Receiver<ServoSrcMsg>, + swap_chain: SwapChain, + servo: Servo<ServoSrcWindow>, +} + +impl ServoThread { + fn new(receiver: Receiver<ServoSrcMsg>) -> Self { + let embedder = Box::new(ServoSrcEmbedder); + let window = Rc::new(ServoSrcWindow::new()); + let swap_chain = window.swap_chain.clone(); + let servo = Servo::new(embedder, window); + Self { + receiver, + swap_chain, + servo, + } + } + + fn run(&mut self) { + self.new_browser(); + while let Ok(msg) = self.receiver.recv() { + debug!("Servo thread handling message {:?}", msg); + match msg { + ServoSrcMsg::GetSwapChain(sender) => sender + .send(self.swap_chain.clone()) + .expect("Failed to send swap chain"), + ServoSrcMsg::Resize(size) => self.resize(size), + ServoSrcMsg::Heartbeat => self.servo.handle_events(vec![]), + ServoSrcMsg::Quit => break, + } + } + self.servo.handle_events(vec![WindowEvent::Quit]); + } + + fn new_browser(&mut self) { + let id = TopLevelBrowsingContextId::new(); + let url = ServoUrl::parse(DEFAULT_URL).unwrap(); + self.servo + .handle_events(vec![WindowEvent::NewBrowser(url, id)]); + } + + fn resize(&mut self, size: Size2D<i32, DevicePixel>) { + GFX.with(|gfx| { + let mut gfx = gfx.borrow_mut(); + let gfx = &mut *gfx; + self.swap_chain + .resize(&mut gfx.device, &mut gfx.context, size.to_untyped()) + .expect("Failed to resize"); + gfx.gl.viewport(0, 0, size.width, size.height); + let fbo = gfx + .device + .context_surface_info(&gfx.context) + .expect("Failed to get context info") + .expect("Failed to get context info") + .framebuffer_object; + gfx.gl.bind_framebuffer(gl::FRAMEBUFFER, fbo); + debug_assert_eq!( + ( + gfx.gl.check_framebuffer_status(gl::FRAMEBUFFER), + gfx.gl.get_error() + ), + (gl::FRAMEBUFFER_COMPLETE, gl::NO_ERROR) + ); + }); + self.servo.handle_events(vec![WindowEvent::Resize]); + } +} + +impl Drop for ServoThread { + fn drop(&mut self) { + GFX.with(|gfx| { + let mut gfx = gfx.borrow_mut(); + let gfx = &mut *gfx; + self.swap_chain + .destroy(&mut gfx.device, &mut gfx.context) + .expect("Failed to destroy swap chain") + }) + } +} + +struct ServoSrcEmbedder; + +impl EmbedderMethods for ServoSrcEmbedder { + fn create_event_loop_waker(&mut self) -> Box<dyn EventLoopWaker> { + Box::new(ServoSrcEmbedder) + } +} + +impl EventLoopWaker for ServoSrcEmbedder { + fn clone_box(&self) -> Box<dyn EventLoopWaker> { + Box::new(ServoSrcEmbedder) + } + + fn wake(&self) {} +} + +struct ServoSrcWindow { + swap_chain: SwapChain, + gl: Rc<dyn gleam::gl::Gl>, +} + +impl ServoSrcWindow { + fn new() -> Self { + GFX.with(|gfx| { + let mut gfx = gfx.borrow_mut(); + let gfx = &mut *gfx; + let access = SurfaceAccess::GPUCPU; + gfx.device + .make_context_current(&mut gfx.context) + .expect("Failed to make context current"); + debug_assert_eq!( + ( + gfx.gl.check_framebuffer_status(gl::FRAMEBUFFER), + gfx.gl.get_error() + ), + (gl::FRAMEBUFFER_COMPLETE, gl::NO_ERROR) + ); + let swap_chain = SwapChain::create_attached(&mut gfx.device, &mut gfx.context, access) + .expect("Failed to create swap chain"); + let fbo = gfx + .device + .context_surface_info(&gfx.context) + .expect("Failed to get context info") + .expect("Failed to get context info") + .framebuffer_object; + gfx.gl.bind_framebuffer(gl::FRAMEBUFFER, fbo); + debug_assert_eq!( + ( + gfx.gl.check_framebuffer_status(gl::FRAMEBUFFER), + gfx.gl.get_error() + ), + (gl::FRAMEBUFFER_COMPLETE, gl::NO_ERROR) + ); + let gl = unsafe { + gleam::gl::GlFns::load_with(|s| gfx.device.get_proc_address(&gfx.context, s)) + }; + Self { swap_chain, gl } + }) + } +} + +impl WindowMethods for ServoSrcWindow { + fn present(&self) { + GFX.with(|gfx| { + debug!("EMBEDDER present"); + let mut gfx = gfx.borrow_mut(); + let gfx = &mut *gfx; + gfx.device + .make_context_current(&mut gfx.context) + .expect("Failed to make context current"); + debug_assert_eq!( + ( + gfx.gl.check_framebuffer_status(gl::FRAMEBUFFER), + gfx.gl.get_error() + ), + (gl::FRAMEBUFFER_COMPLETE, gl::NO_ERROR) + ); + let _ = self + .swap_chain + .swap_buffers(&mut gfx.device, &mut gfx.context); + let fbo = gfx + .device + .context_surface_info(&gfx.context) + .expect("Failed to get context info") + .expect("Failed to get context info") + .framebuffer_object; + gfx.gl.bind_framebuffer(gl::FRAMEBUFFER, fbo); + debug_assert_eq!( + ( + gfx.gl.check_framebuffer_status(gl::FRAMEBUFFER), + gfx.gl.get_error() + ), + (gl::FRAMEBUFFER_COMPLETE, gl::NO_ERROR) + ); + let _ = gfx.device.make_no_context_current(); + }) + } + + fn make_gl_context_current(&self) { + GFX.with(|gfx| { + debug!("EMBEDDER make_context_current"); + let mut gfx = gfx.borrow_mut(); + let gfx = &mut *gfx; + gfx.device + .make_context_current(&mut gfx.context) + .expect("Failed to make context current"); + debug_assert_eq!( + ( + gfx.gl.check_framebuffer_status(gl::FRAMEBUFFER), + gfx.gl.get_error() + ), + (gl::FRAMEBUFFER_COMPLETE, gl::NO_ERROR) + ); + }) + } + + fn gl(&self) -> Rc<dyn gleam::gl::Gl> { + self.gl.clone() + } + + fn get_coordinates(&self) -> EmbedderCoordinates { + let size = Size2D::from_untyped(self.swap_chain.size()); + info!("EMBEDDER coordinates {}", size); + let origin = Point2D::origin(); + EmbedderCoordinates { + hidpi_factor: Scale::new(1.0), + screen: size, + screen_avail: size, + window: (size, origin), + framebuffer: size, + viewport: Rect::new(origin, size), + } + } + + fn set_animation_state(&self, _: AnimationState) {} + + fn get_gl_context(&self) -> servo_media::player::context::GlContext { + servo_media::player::context::GlContext::Unknown + } + + fn get_native_display(&self) -> servo_media::player::context::NativeDisplay { + servo_media::player::context::NativeDisplay::Unknown + } + + fn get_gl_api(&self) -> servo_media::player::context::GlApi { + servo_media::player::context::GlApi::OpenGL3 + } +} + +impl ObjectSubclass for ServoSrc { + const NAME: &'static str = "ServoSrc"; + // gstreamer-gl doesn't have support for GLBaseSrc yet + // https://gitlab.freedesktop.org/gstreamer/gstreamer-rs/issues/219 + type ParentType = BaseSrc; + type Instance = ElementInstanceStruct<Self>; + type Class = ClassStruct<Self>; + + fn new() -> Self { + let (sender, receiver) = crossbeam_channel::bounded(1); + thread::spawn(move || ServoThread::new(receiver).run()); + let (acks, ackr) = crossbeam_channel::bounded(1); + let _ = sender.send(ServoSrcMsg::GetSwapChain(acks)); + let swap_chain = ackr.recv().expect("Failed to get swap chain"); + let info = Mutex::new(None); + Self { + sender, + swap_chain, + info, + } + } + + fn class_init(klass: &mut ClassStruct<Self>) { + klass.set_metadata( + "Servo as a gstreamer src", + "Filter/Effect/Converter/Video", + "The Servo web browser", + env!("CARGO_PKG_AUTHORS"), + ); + + let src_caps = Caps::new_simple( + "video/x-raw", + &[ + ("format", &VideoFormat::Bgrx.to_string()), + ("width", &IntRange::<i32>::new(1, std::i32::MAX)), + ("height", &IntRange::<i32>::new(1, std::i32::MAX)), + ( + "framerate", + &FractionRange::new( + Fraction::new(1, std::i32::MAX), + Fraction::new(std::i32::MAX, 1), + ), + ), + ], + ); + let src_pad_template = + PadTemplate::new("src", PadDirection::Src, PadPresence::Always, &src_caps).unwrap(); + klass.add_pad_template(src_pad_template); + } + + glib_object_subclass!(); +} + +impl ObjectImpl for ServoSrc { + glib_object_impl!(); + + fn constructed(&self, obj: &glib::Object) { + self.parent_constructed(obj); + let basesrc = obj.downcast_ref::<BaseSrc>().unwrap(); + basesrc.set_live(true); + basesrc.set_format(Format::Time); + basesrc.set_do_timestamp(true); + } +} + +impl ElementImpl for ServoSrc {} + +impl BaseSrcImpl for ServoSrc { + fn set_caps(&self, _src: &BaseSrc, outcaps: &Caps) -> Result<(), LoggableError> { + let info = VideoInfo::from_caps(outcaps) + .ok_or_else(|| gst_loggable_error!(CATEGORY, "Failed to get video info"))?; + let size = Size2D::new(info.width(), info.height()).to_i32(); + debug!("Setting servosrc buffer size to {}", size,); + self.sender + .send(ServoSrcMsg::Resize(size)) + .map_err(|_| gst_loggable_error!(CATEGORY, "Failed to send video info"))?; + *self.info.lock().unwrap() = Some(info); + Ok(()) + } + + fn get_size(&self, _src: &BaseSrc) -> Option<u64> { + u64::try_from(self.info.lock().ok()?.as_ref()?.size()).ok() + } + + fn start(&self, _src: &BaseSrc) -> Result<(), ErrorMessage> { + info!("Starting"); + Ok(()) + } + + fn stop(&self, _src: &BaseSrc) -> Result<(), ErrorMessage> { + info!("Stopping"); + let _ = self.sender.send(ServoSrcMsg::Quit); + Ok(()) + } + + fn fill( + &self, + src: &BaseSrc, + _offset: u64, + _length: u32, + buffer: &mut BufferRef, + ) -> Result<FlowSuccess, FlowError> { + let guard = self.info.lock().map_err(|_| { + gst_element_error!(src, CoreError::Negotiation, ["Lock poisoned"]); + FlowError::NotNegotiated + })?; + let info = guard.as_ref().ok_or_else(|| { + gst_element_error!(src, CoreError::Negotiation, ["Caps not set yet"]); + FlowError::NotNegotiated + })?; + let mut frame = VideoFrameRef::from_buffer_ref_writable(buffer, info).ok_or_else(|| { + gst_element_error!( + src, + CoreError::Failed, + ["Failed to map output buffer writable"] + ); + FlowError::Error + })?; + let height = frame.height() as i32; + let width = frame.width() as i32; + let format = frame.format(); + debug!( + "Filling servosrc buffer {}x{} {:?} {:?}", + width, height, format, frame, + ); + let data = frame.plane_data_mut(0).unwrap(); + + GFX.with(|gfx| { + let mut gfx = gfx.borrow_mut(); + let gfx = &mut *gfx; + if let Some(surface) = self.swap_chain.take_surface() { + gfx.device.make_context_current(&gfx.context).unwrap(); + debug_assert_eq!( + ( + gfx.gl.check_framebuffer_status(gl::FRAMEBUFFER), + gfx.gl.get_error() + ), + (gl::FRAMEBUFFER_COMPLETE, gl::NO_ERROR) + ); + + gfx.gl.viewport(0, 0, width, height); + debug_assert_eq!( + ( + gfx.gl.check_framebuffer_status(gl::FRAMEBUFFER), + gfx.gl.get_error() + ), + (gl::FRAMEBUFFER_COMPLETE, gl::NO_ERROR) + ); + + let surface_texture = gfx + .device + .create_surface_texture(&mut gfx.context, surface) + .unwrap(); + let texture_id = surface_texture.gl_texture(); + + gfx.gl.bind_framebuffer(gl::FRAMEBUFFER, gfx.fbo); + gfx.gl.framebuffer_texture_2d( + gl::FRAMEBUFFER, + gl::COLOR_ATTACHMENT0, + gfx.device.surface_gl_texture_target(), + texture_id, + 0, + ); + debug_assert_eq!( + ( + gfx.gl.check_framebuffer_status(gl::FRAMEBUFFER), + gfx.gl.get_error() + ), + (gl::FRAMEBUFFER_COMPLETE, gl::NO_ERROR) + ); + + // TODO: use GL memory to avoid readback + gfx.gl.read_pixels_into_buffer( + 0, + 0, + width, + height, + gl::BGRA, + gl::UNSIGNED_BYTE, + data, + ); + debug_assert_eq!( + ( + gfx.gl.check_framebuffer_status(gl::FRAMEBUFFER), + gfx.gl.get_error() + ), + (gl::FRAMEBUFFER_COMPLETE, gl::NO_ERROR) + ); + + gfx.device.make_no_context_current().unwrap(); + + let surface = gfx + .device + .destroy_surface_texture(&mut gfx.context, surface_texture) + .unwrap(); + self.swap_chain.recycle_surface(surface); + } + }); + let _ = self.sender.send(ServoSrcMsg::Heartbeat); + Ok(FlowSuccess::Ok) + } +} diff --git a/ports/gstplugin/test.html b/ports/gstplugin/test.html new file mode 100644 index 00000000000..a959cd106e8 --- /dev/null +++ b/ports/gstplugin/test.html @@ -0,0 +1,16 @@ +<video controls> + <source src="http://localhost:8080/" type="video/ogg"> +</video> + +<p>Start the video stream with:</p> + +<pre> + gst-launch-1.0 servosrc \ + ! queue \ + ! video/x-raw,framerate=25/1,width=512,height=512 \ + ! videoconvert \ + ! videoflip video-direction=vert \ + ! theoraenc \ + ! oggmux \ + ! tcpserversink host=127.0.0.1 port=8080 +</pre> diff --git a/python/servo/bootstrap.py b/python/servo/bootstrap.py index f65284073e9..e191798cac5 100644 --- a/python/servo/bootstrap.py +++ b/python/servo/bootstrap.py @@ -49,7 +49,7 @@ def install_trusty_deps(force): def check_gstreamer_lib(): - return subprocess.call(["pkg-config", "--atleast-version=1.14", "gstreamer-1.0"], + return subprocess.call(["pkg-config", "--atleast-version=1.16", "gstreamer-1.0"], stdout=PIPE, stderr=PIPE) == 0 diff --git a/python/servo/command_base.py b/python/servo/command_base.py index 5a8818333a9..e6b3ec7908b 100644 --- a/python/servo/command_base.py +++ b/python/servo/command_base.py @@ -583,10 +583,10 @@ class CommandBase(object): return True else: raise Exception("Your system's gstreamer libraries are out of date \ -(we need at least 1.12). Please run ./mach bootstrap-gstreamer") +(we need at least 1.16). Please run ./mach bootstrap-gstreamer") else: raise Exception("Your system's gstreamer libraries are out of date \ -(we need at least 1.12). If you're unable to \ +(we need at least 1.16). If you're unable to \ install them, let us know by filing a bug!") return False @@ -679,7 +679,7 @@ install them, let us know by filing a bug!") # we append in the reverse order so that system gstreamer libraries # do not get precedence extra_path = [libpath] + extra_path - extra_lib = [libpath] + extra_path + extra_lib = [libpath] + extra_lib append_to_path_env(path.join(libpath, "pkgconfig"), env, "PKG_CONFIG_PATH") if sys.platform == "linux2": |