mirror of
https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs.git
synced 2024-11-15 14:52:04 +00:00
gtk4: Refactor and simplify GL context handling
Create a single, global GDK GL context and the corresponding GStreamer GL display and wrapped GStreamer GL context when initializing the first sink and continue using that for all further sinks. Additionally, don't create a full GStreamer GL context inside the sink but only distribute the wrapped GL context in the pipeline so that elements that actually need a full GL context can create one that is sharing with that one. The sink itself does not need a full GStreamer GL context. Then inside the sink check that any GL memory that arrives was created by a GL context that can share with the wrapped GDK GL context and only then use it. And lastly, use the correct GL contexts for a) creating a sync point and b) actually waiting on it. Fixes https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/issues/318 Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/merge_requests/1101>
This commit is contained in:
parent
8f612b9003
commit
5c21b10841
4 changed files with 284 additions and 373 deletions
|
@ -21,7 +21,7 @@ pub(crate) struct Frame {
|
|||
frame: gst_video::VideoFrame<gst_video::video_frame::Readable>,
|
||||
overlays: Vec<Overlay>,
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
gst_context: Option<gst_gl::GLContext>,
|
||||
wrapped_context: Option<gst_gl::GLContext>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -100,7 +100,7 @@ fn video_frame_to_gl_texture(
|
|||
cached_textures: &mut HashMap<usize, gdk::Texture>,
|
||||
used_textures: &mut HashSet<usize>,
|
||||
gdk_context: &gdk::GLContext,
|
||||
gst_context: &gst_gl::GLContext,
|
||||
wrapped_context: &gst_gl::GLContext,
|
||||
) -> (gdk::Texture, f64) {
|
||||
let texture_id = frame.texture_id(0).expect("Invalid texture id") as usize;
|
||||
|
||||
|
@ -116,7 +116,7 @@ fn video_frame_to_gl_texture(
|
|||
let height = frame.height();
|
||||
|
||||
let sync_meta = frame.buffer().meta::<gst_gl::GLSyncMeta>().unwrap();
|
||||
sync_meta.wait(gst_context);
|
||||
sync_meta.wait(wrapped_context);
|
||||
|
||||
let texture = unsafe {
|
||||
gdk::GLTexture::with_release_func(
|
||||
|
@ -156,19 +156,21 @@ impl Frame {
|
|||
}
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
{
|
||||
if let (Some(gdk_ctx), Some(gst_ctx)) = (gdk_context, self.gst_context.as_ref()) {
|
||||
if let (Some(gdk_ctx), Some(wrapped_ctx)) =
|
||||
(gdk_context, self.wrapped_context.as_ref())
|
||||
{
|
||||
video_frame_to_gl_texture(
|
||||
self.frame,
|
||||
cached_textures,
|
||||
&mut used_textures,
|
||||
gdk_ctx,
|
||||
gst_ctx,
|
||||
wrapped_ctx,
|
||||
)
|
||||
} else {
|
||||
// This will fail badly if the video frame was actually mapped as GL texture
|
||||
// but this case can't really happen as we only do that if we actually have a
|
||||
// GDK GL context.
|
||||
assert!(self.gst_context.is_none());
|
||||
assert!(self.wrapped_context.is_none());
|
||||
video_frame_to_memory_texture(self.frame, cached_textures, &mut used_textures)
|
||||
}
|
||||
}
|
||||
|
@ -208,7 +210,12 @@ impl Frame {
|
|||
pub(crate) fn new(
|
||||
buffer: &gst::Buffer,
|
||||
info: &gst_video::VideoInfo,
|
||||
#[allow(unused_variables)] have_gl_context: bool,
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))] wrapped_context: Option<
|
||||
&gst_gl::GLContext,
|
||||
>,
|
||||
#[allow(unused_variables)]
|
||||
#[cfg(not(any(target_os = "macos", feature = "gst_gl")))]
|
||||
wrapped_context: Option<&()>,
|
||||
) -> Result<Self, gst::FlowError> {
|
||||
// Empty buffers get filtered out in show_frame
|
||||
debug_assert!(buffer.n_memory() > 0);
|
||||
|
@ -225,35 +232,35 @@ impl Frame {
|
|||
}
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
{
|
||||
let is_buffer_gl = buffer
|
||||
// Check we received a buffer with GL memory and if the context of that memory
|
||||
// can share with the wrapped context around the GDK GL context.
|
||||
//
|
||||
// If not it has to be uploaded to the GPU.
|
||||
let memory_ctx = buffer
|
||||
.peek_memory(0)
|
||||
.downcast_memory_ref::<gst_gl::GLBaseMemory>()
|
||||
.is_some();
|
||||
|
||||
if !is_buffer_gl || !have_gl_context {
|
||||
frame = Self {
|
||||
frame: gst_video::VideoFrame::from_buffer_readable(buffer.clone(), info)
|
||||
.map_err(|_| gst::FlowError::Error)?,
|
||||
overlays: vec![],
|
||||
gst_context: None,
|
||||
};
|
||||
} else {
|
||||
let gst_ctx = buffer
|
||||
.peek_memory(0)
|
||||
.downcast_memory_ref::<gst_gl::GLBaseMemory>()
|
||||
.map(|m| m.context())
|
||||
.expect("Failed to retrieve the GstGL Context.");
|
||||
.and_then(|m| {
|
||||
let ctx = m.context();
|
||||
if wrapped_context
|
||||
.map_or(false, |wrapped_context| wrapped_context.can_share(ctx))
|
||||
{
|
||||
Some(ctx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(memory_ctx) = memory_ctx {
|
||||
let mapped_frame = if let Some(meta) = buffer.meta::<gst_gl::GLSyncMeta>() {
|
||||
meta.set_sync_point(gst_ctx);
|
||||
meta.set_sync_point(memory_ctx);
|
||||
gst_video::VideoFrame::from_buffer_readable_gl(buffer.clone(), info)
|
||||
.map_err(|_| gst::FlowError::Error)?
|
||||
} else {
|
||||
let mut buffer = buffer.clone();
|
||||
{
|
||||
let buffer = buffer.make_mut();
|
||||
let meta = gst_gl::GLSyncMeta::add(buffer, gst_ctx);
|
||||
meta.set_sync_point(gst_ctx);
|
||||
let meta = gst_gl::GLSyncMeta::add(buffer, memory_ctx);
|
||||
meta.set_sync_point(memory_ctx);
|
||||
}
|
||||
gst_video::VideoFrame::from_buffer_readable_gl(buffer, info)
|
||||
.map_err(|_| gst::FlowError::Error)?
|
||||
|
@ -262,7 +269,14 @@ impl Frame {
|
|||
frame = Self {
|
||||
frame: mapped_frame,
|
||||
overlays: vec![],
|
||||
gst_context: Some(gst_ctx.clone()),
|
||||
wrapped_context: Some(wrapped_context.unwrap().clone()),
|
||||
};
|
||||
} else {
|
||||
frame = Self {
|
||||
frame: gst_video::VideoFrame::from_buffer_readable(buffer.clone(), info)
|
||||
.map_err(|_| gst::FlowError::Error)?,
|
||||
overlays: vec![],
|
||||
wrapped_context: None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,8 +30,23 @@ use crate::utils;
|
|||
use gst_gl::prelude::GLContextExt as GstGLContextExt;
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
use gst_gl::prelude::*;
|
||||
|
||||
// Global GL context that is created by the first sink and kept around until the end of the
|
||||
// process. This is provided to other elements in the pipeline to make sure they create GL contexts
|
||||
// that are sharing with the GTK GL context.
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
enum GLContext {
|
||||
Uninitialized,
|
||||
Unsupported,
|
||||
Initialized {
|
||||
display: gst_gl::GLDisplay,
|
||||
wrapped_context: gst_gl::GLContext,
|
||||
gdk_context: ThreadGuard<gdk::GLContext>,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
static GL_CONTEXT: Mutex<GLContext> = Mutex::new(GLContext::Uninitialized);
|
||||
|
||||
static CAT: Lazy<gst::DebugCategory> = Lazy::new(|| {
|
||||
gst::DebugCategory::new(
|
||||
|
@ -47,15 +62,7 @@ pub struct PaintableSink {
|
|||
info: Mutex<Option<gst_video::VideoInfo>>,
|
||||
sender: Mutex<Option<Sender<SinkEvent>>>,
|
||||
pending_frame: Mutex<Option<Frame>>,
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
gst_display: Mutex<Option<gst_gl::GLDisplay>>,
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
gst_app_context: Mutex<Option<gst_gl::GLContext>>,
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
gst_context: Mutex<Option<gst_gl::GLContext>>,
|
||||
cached_caps: Mutex<Option<gst::Caps>>,
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
have_gl_context: AtomicBool,
|
||||
}
|
||||
|
||||
impl Drop for PaintableSink {
|
||||
|
@ -147,7 +154,9 @@ impl ElementImpl for PaintableSink {
|
|||
|
||||
for features in [
|
||||
None,
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
Some(&["memory:GLMemory", "meta:GstVideoOverlayComposition"][..]),
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
Some(&["memory:GLMemory"][..]),
|
||||
Some(&["memory:SystemMemory", "meta:GstVideoOverlayComposition"][..]),
|
||||
Some(&["meta:GstVideoOverlayComposition"][..]),
|
||||
|
@ -210,16 +219,33 @@ impl ElementImpl for PaintableSink {
|
|||
|
||||
drop(paintable);
|
||||
|
||||
// Notify the pipeline about the GL display and wrapped context so that any other
|
||||
// elements in the pipeline ideally use the same / create GL contexts that are
|
||||
// sharing with this one.
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
{
|
||||
if self.have_gl_context.load(Ordering::Relaxed) {
|
||||
if self.initialize_gl_wrapper() {
|
||||
// We must have a display at this point.
|
||||
let display = self.gst_display.lock().unwrap().clone().unwrap();
|
||||
gst_gl::gl_element_propagate_display_context(&*self.obj(), &display);
|
||||
} else {
|
||||
self.have_gl_context.store(false, Ordering::Relaxed);
|
||||
let gl_context = GL_CONTEXT.lock().unwrap();
|
||||
if let GLContext::Initialized {
|
||||
display,
|
||||
wrapped_context,
|
||||
..
|
||||
} = &*gl_context
|
||||
{
|
||||
let display = display.clone();
|
||||
let wrapped_context = wrapped_context.clone();
|
||||
drop(gl_context);
|
||||
|
||||
gst_gl::gl_element_propagate_display_context(&*self.obj(), &display);
|
||||
let mut ctx = gst::Context::new("gst.gl.app_context", true);
|
||||
{
|
||||
let ctx = ctx.get_mut().unwrap();
|
||||
ctx.structure_mut().set("context", &wrapped_context);
|
||||
}
|
||||
let _ = self.obj().post_message(
|
||||
gst::message::HaveContext::builder(ctx)
|
||||
.src(&*self.obj())
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -243,12 +269,6 @@ impl ElementImpl for PaintableSink {
|
|||
}
|
||||
});
|
||||
}
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
gst::StateChange::ReadyToNull => {
|
||||
let _ = self.gst_context.lock().unwrap().take();
|
||||
let _ = self.gst_app_context.lock().unwrap().take();
|
||||
let _ = self.gst_display.lock().unwrap().take();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
|
@ -269,25 +289,24 @@ impl BaseSinkImpl for PaintableSink {
|
|||
templ[0].caps().clone()
|
||||
});
|
||||
|
||||
gst::debug!(CAT, imp: self, "Advertising our own caps: {:?}", &tmp_caps);
|
||||
gst::debug!(CAT, imp: self, "Advertising our own caps: {tmp_caps:?}");
|
||||
|
||||
if let Some(filter_caps) = filter {
|
||||
gst::debug!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Intersecting with filter caps: {:?}",
|
||||
&filter_caps
|
||||
"Intersecting with filter caps: {filter_caps:?}",
|
||||
);
|
||||
|
||||
tmp_caps = filter_caps.intersect_with_mode(&tmp_caps, gst::CapsIntersectMode::First);
|
||||
};
|
||||
|
||||
gst::debug!(CAT, imp: self, "Returning caps: {:?}", &tmp_caps);
|
||||
gst::debug!(CAT, imp: self, "Returning caps: {tmp_caps:?}");
|
||||
Some(tmp_caps)
|
||||
}
|
||||
|
||||
fn set_caps(&self, caps: &gst::Caps) -> Result<(), gst::LoggableError> {
|
||||
gst::debug!(CAT, imp: self, "Setting caps {:?}", caps);
|
||||
gst::debug!(CAT, imp: self, "Setting caps {caps:?}");
|
||||
|
||||
let video_info = gst_video::VideoInfo::from_caps(caps)
|
||||
.map_err(|_| gst::loggable_error!(CAT, "Invalid caps"))?;
|
||||
|
@ -309,78 +328,21 @@ impl BaseSinkImpl for PaintableSink {
|
|||
// TODO: Provide a preferred "window size" here for higher-resolution rendering
|
||||
query.add_allocation_meta::<gst_video::VideoOverlayCompositionMeta>(None);
|
||||
|
||||
#[cfg(not(any(target_os = "macos", feature = "gst_gl")))]
|
||||
{
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
{
|
||||
// Early return if there is no context initialized
|
||||
let gst_context = match &*self.gst_context.lock().unwrap() {
|
||||
None => {
|
||||
gst::debug!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Found no GL Context during propose_allocation."
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
Some(gst_context) => gst_context.clone(),
|
||||
};
|
||||
|
||||
// GL specific things
|
||||
let (caps, need_pool) = query.get_owned();
|
||||
if caps.is_empty() || caps.is_any() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(f) = caps.features(0) {
|
||||
if !f.contains("memory:GLMemory") {
|
||||
gst::debug!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"No 'memory:GLMemory' feature in caps: {}",
|
||||
caps
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let info = gst_video::VideoInfo::from_caps(&caps)
|
||||
.map_err(|_| gst::loggable_error!(CAT, "Failed to get VideoInfo from caps"))?;
|
||||
|
||||
let size = info.size() as u32;
|
||||
let buffer_pool = if need_pool {
|
||||
let buffer_pool = gst_gl::GLBufferPool::new(&gst_context);
|
||||
gst::debug!(CAT, imp: self, "Creating new Pool");
|
||||
|
||||
let mut config = buffer_pool.config();
|
||||
config.set_params(Some(&caps), size, 0, 0);
|
||||
config.add_option("GstBufferPoolOptionGLSyncMeta");
|
||||
|
||||
if let Err(err) = buffer_pool.set_config(config) {
|
||||
return Err(gst::loggable_error!(
|
||||
CAT,
|
||||
format!("Failed to set config in the GL BufferPool.: {}", err)
|
||||
));
|
||||
}
|
||||
|
||||
Some(buffer_pool)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// we need at least 2 buffer because we hold on to the last one
|
||||
query.add_allocation_pool(buffer_pool.as_ref(), size, 2, 0);
|
||||
|
||||
if gst_context.check_feature("GL_ARB_sync")
|
||||
|| gst_context.check_feature("GL_EXT_EGL_sync")
|
||||
if let GLContext::Initialized {
|
||||
wrapped_context, ..
|
||||
} = &*GL_CONTEXT.lock().unwrap()
|
||||
{
|
||||
query.add_allocation_meta::<gst_gl::GLSyncMeta>(None)
|
||||
if wrapped_context.check_feature("GL_ARB_sync")
|
||||
|| wrapped_context.check_feature("GL_EXT_EGL_sync")
|
||||
{
|
||||
query.add_allocation_meta::<gst_gl::GLSyncMeta>(None)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn query(&self, query: &mut gst::QueryRef) -> bool {
|
||||
|
@ -391,21 +353,27 @@ impl BaseSinkImpl for PaintableSink {
|
|||
gst::QueryViewMut::Context(q) => {
|
||||
// Avoid holding the locks while we respond to the query
|
||||
// The objects are ref-counted anyway.
|
||||
let (gst_display, app_ctx, gst_ctx) = (
|
||||
self.gst_display.lock().unwrap().clone(),
|
||||
self.gst_app_context.lock().unwrap().clone(),
|
||||
self.gst_context.lock().unwrap().clone(),
|
||||
);
|
||||
let mut display_clone = None;
|
||||
let mut wrapped_context_clone = None;
|
||||
if let GLContext::Initialized {
|
||||
display,
|
||||
wrapped_context,
|
||||
..
|
||||
} = &*GL_CONTEXT.lock().unwrap()
|
||||
{
|
||||
display_clone = Some(display.clone());
|
||||
wrapped_context_clone = Some(wrapped_context.clone());
|
||||
}
|
||||
|
||||
if let (Some(gst_display), Some(app_ctx), Some(gst_ctx)) =
|
||||
(gst_display, app_ctx, gst_ctx)
|
||||
if let (Some(display), Some(wrapped_context)) =
|
||||
(display_clone, wrapped_context_clone)
|
||||
{
|
||||
return gst_gl::functions::gl_handle_context_query(
|
||||
&*self.obj(),
|
||||
q,
|
||||
Some(&gst_display),
|
||||
Some(&gst_ctx),
|
||||
Some(&app_ctx),
|
||||
Some(&display),
|
||||
None::<&gst_gl::GLContext>,
|
||||
Some(&wrapped_context),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -436,17 +404,25 @@ impl VideoSinkImpl for PaintableSink {
|
|||
gst::FlowError::NotNegotiated
|
||||
})?;
|
||||
|
||||
let have_gl_context = {
|
||||
let wrapped_context = {
|
||||
#[cfg(not(any(target_os = "macos", feature = "gst_gl")))]
|
||||
{
|
||||
false
|
||||
None
|
||||
}
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
{
|
||||
self.have_gl_context.load(Ordering::Relaxed)
|
||||
let gl_context = GL_CONTEXT.lock().unwrap();
|
||||
if let GLContext::Initialized {
|
||||
wrapped_context, ..
|
||||
} = &*gl_context
|
||||
{
|
||||
Some(wrapped_context.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
let frame = Frame::new(buffer, info, have_gl_context).map_err(|err| {
|
||||
let frame = Frame::new(buffer, info, wrapped_context.as_ref()).map_err(|err| {
|
||||
gst::error!(CAT, imp: self, "Failed to map video frame");
|
||||
err
|
||||
})?;
|
||||
|
@ -498,7 +474,7 @@ impl PaintableSink {
|
|||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
{
|
||||
// Filter out GL caps from the template pads if we have no context
|
||||
if !self.have_gl_context.load(Ordering::Relaxed) {
|
||||
if !matches!(&*GL_CONTEXT.lock().unwrap(), GLContext::Initialized { .. }) {
|
||||
tmp_caps = tmp_caps
|
||||
.iter_with_features()
|
||||
.filter(|(_, features)| !features.contains("memory:GLMemory"))
|
||||
|
@ -514,32 +490,37 @@ impl PaintableSink {
|
|||
}
|
||||
|
||||
fn create_paintable(&self, paintable_storage: &mut MutexGuard<Option<ThreadGuard<Paintable>>>) {
|
||||
#[allow(unused_mut)]
|
||||
let mut ctx = None;
|
||||
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
{
|
||||
if let Some(c) = self.realize_context() {
|
||||
self.have_gl_context.store(true, Ordering::Relaxed);
|
||||
ctx = Some(c);
|
||||
}
|
||||
self.initialize_gl_context();
|
||||
}
|
||||
|
||||
self.configure_caps();
|
||||
self.initialize_paintable(ctx, paintable_storage);
|
||||
self.initialize_paintable(paintable_storage);
|
||||
}
|
||||
|
||||
fn initialize_paintable(
|
||||
&self,
|
||||
gl_context: Option<ThreadGuard<gdk::GLContext>>,
|
||||
paintable_storage: &mut MutexGuard<Option<ThreadGuard<Paintable>>>,
|
||||
) {
|
||||
gst::debug!(CAT, imp: self, "Initializing paintable");
|
||||
|
||||
let paintable = utils::invoke_on_main_thread(|| {
|
||||
// grab the context out of the fragile
|
||||
let ctx = gl_context.map(|f| f.into_inner());
|
||||
ThreadGuard::new(Paintable::new(ctx))
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
{
|
||||
let gdk_context = if let GLContext::Initialized { gdk_context, .. } =
|
||||
&*GL_CONTEXT.lock().unwrap()
|
||||
{
|
||||
Some(gdk_context.get_ref().clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
ThreadGuard::new(Paintable::new(gdk_context))
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", feature = "gst_gl")))]
|
||||
{
|
||||
ThreadGuard::new(Paintable::new(None))
|
||||
}
|
||||
});
|
||||
|
||||
// The channel for the SinkEvents
|
||||
|
@ -559,207 +540,144 @@ impl PaintableSink {
|
|||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
fn realize_context(&self) -> Option<ThreadGuard<gdk::GLContext>> {
|
||||
fn initialize_gl_context(&self) {
|
||||
gst::debug!(CAT, imp: self, "Realizing GDK GL Context");
|
||||
|
||||
let self_ = self.to_owned();
|
||||
utils::invoke_on_main_thread(move || -> Option<ThreadGuard<gdk::GLContext>> {
|
||||
gst::debug!(
|
||||
CAT,
|
||||
imp: self_,
|
||||
"Realizing GDK GL Context from main context"
|
||||
);
|
||||
|
||||
// This can return NULL but only happens in 2 situations:
|
||||
// * If the function is called before gtk_init
|
||||
// * If the function is called after gdk_display_close(default_display)
|
||||
// Both of which are treated as programming errors.
|
||||
//
|
||||
// However, when we are building the docs, gtk_init doesn't get called
|
||||
// and this would cause the documentation generation to error.
|
||||
// Thus its okayish to return None here and fallback to software
|
||||
// rendering, since this path isn't going to be used by applications
|
||||
// anyway.
|
||||
//
|
||||
// FIXME: add a couple more gtk_init checks across the codebase where
|
||||
// applicable since this is no longer going to panic.
|
||||
let display = gdk::Display::default()?;
|
||||
let ctx = match display.create_gl_context() {
|
||||
Ok(ctx) => ctx,
|
||||
Err(err) => {
|
||||
gst::warning!(CAT, imp: self_, "Failed to create GDK GL Context: {err}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
match ctx.type_().name() {
|
||||
#[cfg(all(target_os = "linux", feature = "x11egl"))]
|
||||
"GdkX11GLContextEGL" => (),
|
||||
#[cfg(all(target_os = "linux", feature = "x11glx"))]
|
||||
"GdkX11GLContextGLX" => (),
|
||||
#[cfg(all(target_os = "linux", feature = "wayland"))]
|
||||
"GdkWaylandGLContext" => (),
|
||||
#[cfg(target_os = "macos")]
|
||||
"GdkMacosGLContext" => (),
|
||||
display => {
|
||||
gst::error!(CAT, imp: self_, "Unsupported GDK display {display} for GL");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
gst::info!(CAT, imp: &self_, "Realizing GDK GL Context",);
|
||||
|
||||
match ctx.realize() {
|
||||
Ok(_) => {
|
||||
gst::info!(CAT, imp: self_, "Successfully realized GDK GL Context",);
|
||||
Some(ThreadGuard::new(ctx))
|
||||
}
|
||||
Err(err) => {
|
||||
gst::warning!(CAT, imp: self_, "Failed to realize GDK GL Context: {err}",);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
utils::invoke_on_main_thread(move || {
|
||||
self_.initialize_gl_context_main();
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
fn initialize_gl_wrapper(&self) -> bool {
|
||||
gst::info!(CAT, imp: self, "Initializing GDK GL Context");
|
||||
let self_ = self.to_owned();
|
||||
utils::invoke_on_main_thread(move || self_.initialize_gl())
|
||||
}
|
||||
fn initialize_gl_context_main(&self) {
|
||||
gst::debug!(CAT, imp: self, "Realizing GDK GL Context from main thread");
|
||||
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
fn initialize_gl(&self) -> bool {
|
||||
let ctx = {
|
||||
let paintable = self.paintable.lock().unwrap();
|
||||
// Impossible to not have a paintable and GL context at this point
|
||||
paintable.as_ref().unwrap().get_ref().context().unwrap()
|
||||
};
|
||||
let mut gl_context_guard = GL_CONTEXT.lock().unwrap();
|
||||
if !matches!(&*gl_context_guard, GLContext::Uninitialized) {
|
||||
gst::debug!(CAT, imp: self, "Already initialized GL context before");
|
||||
return;
|
||||
}
|
||||
*gl_context_guard = GLContext::Unsupported;
|
||||
|
||||
let display = gtk::prelude::GLContextExt::display(&ctx)
|
||||
.expect("Failed to get GDK Display from GDK Context.");
|
||||
ctx.make_current();
|
||||
|
||||
let mut app_ctx_guard = self.gst_app_context.lock().unwrap();
|
||||
let mut display_guard = self.gst_display.lock().unwrap();
|
||||
|
||||
match ctx.type_().name() {
|
||||
#[cfg(all(target_os = "linux", feature = "x11egl"))]
|
||||
"GdkX11GLContextEGL" => {
|
||||
self.initialize_x11egl(display, &mut display_guard, &mut app_ctx_guard);
|
||||
}
|
||||
#[cfg(all(target_os = "linux", feature = "x11glx"))]
|
||||
"GdkX11GLContextGLX" => {
|
||||
self.initialize_x11glx(display, &mut display_guard, &mut app_ctx_guard);
|
||||
}
|
||||
#[cfg(all(target_os = "linux", feature = "wayland"))]
|
||||
"GdkWaylandGLContext" => {
|
||||
self.initialize_waylandegl(display, &mut display_guard, &mut app_ctx_guard);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
"GdkMacosGLContext" => {
|
||||
self.initialize_macosgl(display, &mut display_guard, &mut app_ctx_guard);
|
||||
}
|
||||
_ => {
|
||||
unreachable!("Unsupported GDK display {display} for GL");
|
||||
}
|
||||
};
|
||||
|
||||
// This should have been initialized once we are done with the platform checks
|
||||
let app_ctx = match &*app_ctx_guard {
|
||||
None => {
|
||||
assert!(display_guard.is_none());
|
||||
return false;
|
||||
}
|
||||
Some(app_ctx) => app_ctx,
|
||||
};
|
||||
|
||||
let display = match &*display_guard {
|
||||
None => return false,
|
||||
// This can return NULL but only happens in 2 situations:
|
||||
// * If the function is called before gtk_init
|
||||
// * If the function is called after gdk_display_close(default_display)
|
||||
// Both of which are treated as programming errors.
|
||||
//
|
||||
// However, when we are building the docs, gtk_init doesn't get called
|
||||
// and this would cause the documentation generation to error.
|
||||
// Thus its okayish to return None here and fallback to software
|
||||
// rendering, since this path isn't going to be used by applications
|
||||
// anyway.
|
||||
//
|
||||
// FIXME: add a couple more gtk_init checks across the codebase where
|
||||
// applicable since this is no longer going to panic.
|
||||
let gdk_display = match gdk::Display::default() {
|
||||
Some(display) => display,
|
||||
None => {
|
||||
gst::warning!(CAT, imp: self, "Failed to retrieve GDK display");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let gdk_context = match gdk_display.create_gl_context() {
|
||||
Ok(gdk_context) => gdk_context,
|
||||
Err(err) => {
|
||||
gst::warning!(CAT, imp: self, "Failed to create GDK GL Context: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match app_ctx.activate(true) {
|
||||
Ok(_) => gst::info!(CAT, imp: self, "Successfully activated GL Context."),
|
||||
match gdk_context.type_().name() {
|
||||
#[cfg(all(target_os = "linux", feature = "x11egl"))]
|
||||
"GdkX11GLContextEGL" => (),
|
||||
#[cfg(all(target_os = "linux", feature = "x11glx"))]
|
||||
"GdkX11GLContextGLX" => (),
|
||||
#[cfg(all(target_os = "linux", feature = "wayland"))]
|
||||
"GdkWaylandGLContext" => (),
|
||||
#[cfg(target_os = "macos")]
|
||||
"GdkMacosGLContext" => (),
|
||||
display => {
|
||||
gst::error!(CAT, imp: self, "Unsupported GDK display {display} for GL");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
gst::info!(CAT, imp: self, "Realizing GDK GL Context",);
|
||||
|
||||
if let Err(err) = gdk_context.realize() {
|
||||
gst::warning!(CAT, imp: self, "Failed to realize GDK GL Context: {err}");
|
||||
return;
|
||||
}
|
||||
|
||||
gst::info!(CAT, imp: self, "Successfully realized GDK GL Context");
|
||||
|
||||
gdk_context.make_current();
|
||||
|
||||
let res = match gdk_context.type_().name() {
|
||||
#[cfg(all(target_os = "linux", feature = "x11egl"))]
|
||||
"GdkX11GLContextEGL" => self.initialize_x11egl(gdk_display),
|
||||
#[cfg(all(target_os = "linux", feature = "x11glx"))]
|
||||
"GdkX11GLContextGLX" => self.initialize_x11glx(gdk_display),
|
||||
#[cfg(all(target_os = "linux", feature = "wayland"))]
|
||||
"GdkWaylandGLContext" => self.initialize_waylandegl(gdk_display),
|
||||
#[cfg(target_os = "macos")]
|
||||
"GdkMacosGLContext" => self.initialize_macosgl(gdk_display),
|
||||
display_type => {
|
||||
unreachable!("Unsupported GDK display {display_type} for GL");
|
||||
}
|
||||
};
|
||||
|
||||
let (display, wrapped_context) = match res {
|
||||
Some((display, wrapped_context)) => (display, wrapped_context),
|
||||
None => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match wrapped_context.activate(true) {
|
||||
Ok(_) => gst::info!(CAT, imp: self, "Successfully activated GL Context"),
|
||||
Err(_) => {
|
||||
gst::error!(CAT, imp: self, "Failed to activate GL context",);
|
||||
*app_ctx_guard = None;
|
||||
*display_guard = None;
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = app_ctx.fill_info() {
|
||||
if let Err(err) = wrapped_context.fill_info() {
|
||||
gst::error!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Failed to fill info on the GL Context: {err}",
|
||||
);
|
||||
// Deactivate the context upon failure
|
||||
if app_ctx.activate(false).is_err() {
|
||||
if wrapped_context.activate(false).is_err() {
|
||||
gst::error!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Failed to deactivate the context after failing fill info",
|
||||
);
|
||||
}
|
||||
*app_ctx_guard = None;
|
||||
*display_guard = None;
|
||||
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
if app_ctx.activate(false).is_err() {
|
||||
gst::error!(CAT, imp: self, "Failed to deactivate GL context",);
|
||||
*app_ctx_guard = None;
|
||||
*display_guard = None;
|
||||
return false;
|
||||
}
|
||||
gst::info!(CAT, imp: self, "Successfully initialized GL Context");
|
||||
|
||||
gst::info!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Successfully deactivated GL Context after fill_info"
|
||||
);
|
||||
|
||||
let gst_context = match display.create_context(app_ctx) {
|
||||
Ok(gst_context) => gst_context,
|
||||
Err(err) => {
|
||||
gst::error!(CAT, imp: self, "Could not create GL context: {err}");
|
||||
*app_ctx_guard = None;
|
||||
*display_guard = None;
|
||||
return false;
|
||||
}
|
||||
*gl_context_guard = GLContext::Initialized {
|
||||
display,
|
||||
wrapped_context,
|
||||
gdk_context: ThreadGuard::new(gdk_context),
|
||||
};
|
||||
|
||||
match display.add_context(&gst_context) {
|
||||
Ok(_) => {
|
||||
let mut gst_ctx_guard = self.gst_context.lock().unwrap();
|
||||
gst::info!(CAT, imp: self, "Successfully initialized GL Context");
|
||||
gst_ctx_guard.replace(gst_context);
|
||||
true
|
||||
}
|
||||
Err(_) => {
|
||||
gst::error!(CAT, imp: self, "Could not add GL context to display");
|
||||
*app_ctx_guard = None;
|
||||
*display_guard = None;
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "x11egl"))]
|
||||
fn initialize_x11egl(
|
||||
&self,
|
||||
display: gdk::Display,
|
||||
display_guard: &mut Option<gst_gl::GLDisplay>,
|
||||
app_ctx_guard: &mut Option<gst_gl::GLContext>,
|
||||
) {
|
||||
) -> Option<(gst_gl::GLDisplay, gst_gl::GLContext)> {
|
||||
gst::info!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Initializing GL for x11 EGL backend and display."
|
||||
"Initializing GL for x11 EGL backend and display"
|
||||
);
|
||||
|
||||
let platform = gst_gl::GLPlatform::EGL;
|
||||
|
@ -767,37 +685,37 @@ impl PaintableSink {
|
|||
let gl_ctx = gst_gl::GLContext::current_gl_context(platform);
|
||||
|
||||
if gl_ctx == 0 {
|
||||
gst::error!(CAT, imp: self, "Failed to get handle from GdkGLContext",);
|
||||
return;
|
||||
gst::error!(CAT, imp: self, "Failed to get handle from GdkGLContext");
|
||||
return None;
|
||||
}
|
||||
|
||||
// FIXME: bindings
|
||||
unsafe {
|
||||
use glib::translate::*;
|
||||
|
||||
let d = display.downcast::<gdk_x11::X11Display>().unwrap();
|
||||
let x11_display = gdk_x11::ffi::gdk_x11_display_get_egl_display(d.to_glib_none().0);
|
||||
let display = display.downcast::<gdk_x11::X11Display>().unwrap();
|
||||
let x11_display =
|
||||
gdk_x11::ffi::gdk_x11_display_get_egl_display(display.to_glib_none().0);
|
||||
if x11_display.is_null() {
|
||||
gst::error!(CAT, imp: self, "Failed to get EGL display");
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
|
||||
let gst_display = gst_gl_egl::ffi::gst_gl_display_egl_new_with_egl_display(x11_display);
|
||||
let gst_display =
|
||||
gst_gl::GLDisplay::from_glib_full(gst_display as *mut gst_gl::ffi::GstGLDisplay);
|
||||
|
||||
let gst_app_context =
|
||||
let wrapped_context =
|
||||
gst_gl::GLContext::new_wrapped(&gst_display, gl_ctx, platform, gl_api);
|
||||
let gst_app_context = match gst_app_context {
|
||||
let wrapped_context = match wrapped_context {
|
||||
None => {
|
||||
gst::error!(CAT, imp: self, "Failed to create wrapped GL context");
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
Some(gst_app_context) => gst_app_context,
|
||||
Some(wrapped_context) => wrapped_context,
|
||||
};
|
||||
|
||||
display_guard.replace(gst_display);
|
||||
app_ctx_guard.replace(gst_app_context);
|
||||
Some((gst_display, wrapped_context))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -805,13 +723,11 @@ impl PaintableSink {
|
|||
fn initialize_x11glx(
|
||||
&self,
|
||||
display: gdk::Display,
|
||||
display_guard: &mut Option<gst_gl::GLDisplay>,
|
||||
app_ctx_guard: &mut Option<gst_gl::GLContext>,
|
||||
) {
|
||||
) -> Option<(gst_gl::GLDisplay, gst_gl::GLContext)> {
|
||||
gst::info!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Initializing GL for x11 GLX backend and display."
|
||||
"Initializing GL for x11 GLX backend and display"
|
||||
);
|
||||
|
||||
let platform = gst_gl::GLPlatform::GLX;
|
||||
|
@ -819,37 +735,36 @@ impl PaintableSink {
|
|||
let gl_ctx = gst_gl::GLContext::current_gl_context(platform);
|
||||
|
||||
if gl_ctx == 0 {
|
||||
gst::error!(CAT, imp: self, "Failed to get handle from GdkGLContext",);
|
||||
return;
|
||||
gst::error!(CAT, imp: self, "Failed to get handle from GdkGLContext");
|
||||
return None;
|
||||
}
|
||||
|
||||
// FIXME: bindings
|
||||
unsafe {
|
||||
use glib::translate::*;
|
||||
|
||||
let d = display.downcast::<gdk_x11::X11Display>().unwrap();
|
||||
let x11_display = gdk_x11::ffi::gdk_x11_display_get_xdisplay(d.to_glib_none().0);
|
||||
let display = display.downcast::<gdk_x11::X11Display>().unwrap();
|
||||
let x11_display = gdk_x11::ffi::gdk_x11_display_get_xdisplay(display.to_glib_none().0);
|
||||
if x11_display.is_null() {
|
||||
gst::error!(CAT, imp: self, "Failed to get X11 display");
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
|
||||
let gst_display = gst_gl_x11::ffi::gst_gl_display_x11_new_with_display(x11_display);
|
||||
let gst_display =
|
||||
gst_gl::GLDisplay::from_glib_full(gst_display as *mut gst_gl::ffi::GstGLDisplay);
|
||||
|
||||
let gst_app_context =
|
||||
let wrapped_context =
|
||||
gst_gl::GLContext::new_wrapped(&gst_display, gl_ctx, platform, gl_api);
|
||||
let gst_app_context = match gst_app_context {
|
||||
let wrapped_context = match wrapped_context {
|
||||
None => {
|
||||
gst::error!(CAT, imp: self, "Failed to create wrapped GL context");
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
Some(gst_app_context) => gst_app_context,
|
||||
Some(wrapped_context) => wrapped_context,
|
||||
};
|
||||
|
||||
display_guard.replace(gst_display);
|
||||
app_ctx_guard.replace(gst_app_context);
|
||||
Some((gst_display, wrapped_context))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -857,13 +772,11 @@ impl PaintableSink {
|
|||
fn initialize_waylandegl(
|
||||
&self,
|
||||
display: gdk::Display,
|
||||
display_guard: &mut Option<gst_gl::GLDisplay>,
|
||||
app_ctx_guard: &mut Option<gst_gl::GLContext>,
|
||||
) {
|
||||
) -> Option<(gst_gl::GLDisplay, gst_gl::GLContext)> {
|
||||
gst::info!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Initializing GL for Wayland EGL backend and display."
|
||||
"Initializing GL for Wayland EGL backend and display"
|
||||
);
|
||||
|
||||
let platform = gst_gl::GLPlatform::EGL;
|
||||
|
@ -871,8 +784,8 @@ impl PaintableSink {
|
|||
let gl_ctx = gst_gl::GLContext::current_gl_context(platform);
|
||||
|
||||
if gl_ctx == 0 {
|
||||
gst::error!(CAT, imp: self, "Failed to get handle from GdkGLContext",);
|
||||
return;
|
||||
gst::error!(CAT, imp: self, "Failed to get handle from GdkGLContext");
|
||||
return None;
|
||||
}
|
||||
|
||||
// FIXME: bindings
|
||||
|
@ -881,12 +794,12 @@ impl PaintableSink {
|
|||
|
||||
// let wayland_display = gdk_wayland::WaylandDisplay::wl_display(display.downcast());
|
||||
// get the ptr directly since we are going to use it raw
|
||||
let d = display.downcast::<gdk_wayland::WaylandDisplay>().unwrap();
|
||||
let display = display.downcast::<gdk_wayland::WaylandDisplay>().unwrap();
|
||||
let wayland_display =
|
||||
gdk_wayland::ffi::gdk_wayland_display_get_wl_display(d.to_glib_none().0);
|
||||
gdk_wayland::ffi::gdk_wayland_display_get_wl_display(display.to_glib_none().0);
|
||||
if wayland_display.is_null() {
|
||||
gst::error!(CAT, imp: self, "Failed to get Wayland display");
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
|
||||
let gst_display =
|
||||
|
@ -894,19 +807,18 @@ impl PaintableSink {
|
|||
let gst_display =
|
||||
gst_gl::GLDisplay::from_glib_full(gst_display as *mut gst_gl::ffi::GstGLDisplay);
|
||||
|
||||
let gst_app_context =
|
||||
let wrapped_context =
|
||||
gst_gl::GLContext::new_wrapped(&gst_display, gl_ctx, platform, gl_api);
|
||||
|
||||
let gst_app_context = match gst_app_context {
|
||||
let wrapped_context = match wrapped_context {
|
||||
None => {
|
||||
gst::error!(CAT, imp: self, "Failed to create wrapped GL context");
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
Some(gst_app_context) => gst_app_context,
|
||||
Some(wrapped_context) => wrapped_context,
|
||||
};
|
||||
|
||||
display_guard.replace(gst_display);
|
||||
app_ctx_guard.replace(gst_app_context);
|
||||
Some((gst_display, wrapped_context))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -914,13 +826,11 @@ impl PaintableSink {
|
|||
fn initialize_macosgl(
|
||||
&self,
|
||||
display: gdk::Display,
|
||||
display_guard: &mut Option<gst_gl::GLDisplay>,
|
||||
app_ctx_guard: &mut Option<gst_gl::GLContext>,
|
||||
) {
|
||||
) -> Option<(gst_gl::GLDisplay, gst_gl::GLContext)> {
|
||||
gst::info!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Initializing GL for macOS backend and display."
|
||||
"Initializing GL for macOS backend and display"
|
||||
);
|
||||
|
||||
let platform = gst_gl::GLPlatform::CGL;
|
||||
|
@ -928,25 +838,24 @@ impl PaintableSink {
|
|||
let gl_ctx = gst_gl::GLContext::current_gl_context(platform);
|
||||
|
||||
if gl_ctx == 0 {
|
||||
gst::error!(CAT, imp: self, "Failed to get handle from GdkGLContext",);
|
||||
return;
|
||||
gst::error!(CAT, imp: self, "Failed to get handle from GdkGLContext");
|
||||
return None;
|
||||
}
|
||||
|
||||
let gst_display = gst_gl::GLDisplay::new();
|
||||
unsafe {
|
||||
let gst_app_context =
|
||||
let wrapped_context =
|
||||
gst_gl::GLContext::new_wrapped(&gst_display, gl_ctx, platform, gl_api);
|
||||
|
||||
let gst_app_context = match gst_app_context {
|
||||
let wrapped_context = match wrapped_context {
|
||||
None => {
|
||||
gst::error!(CAT, imp: self, "Failed to create wrapped GL context");
|
||||
return;
|
||||
}
|
||||
Some(gst_app_context) => gst_app_context,
|
||||
Some(wrapped_context) => wrapped_context,
|
||||
};
|
||||
|
||||
display_guard.replace(gst_display);
|
||||
app_ctx_guard.replace(gst_app_context);
|
||||
Some((gst_display, wrapped_context))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -165,11 +165,6 @@ impl PaintableImpl for Paintable {
|
|||
}
|
||||
|
||||
impl Paintable {
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
pub(super) fn context(&self) -> Option<gdk::GLContext> {
|
||||
self.gl_context.borrow().clone()
|
||||
}
|
||||
|
||||
pub(super) fn handle_frame_changed(&self, frame: Option<Frame>) {
|
||||
let context = self.gl_context.borrow();
|
||||
if let Some(frame) = frame {
|
||||
|
@ -191,9 +186,7 @@ impl Paintable {
|
|||
gst::debug!(
|
||||
CAT,
|
||||
imp: self,
|
||||
"Size changed from {:?} to {:?}",
|
||||
old_size,
|
||||
new_size,
|
||||
"Size changed from {old_size:?} to {new_size:?}",
|
||||
);
|
||||
self.obj().invalidate_size();
|
||||
}
|
||||
|
|
|
@ -30,11 +30,6 @@ impl Paintable {
|
|||
}
|
||||
|
||||
impl Paintable {
|
||||
#[cfg(any(target_os = "macos", feature = "gst_gl"))]
|
||||
pub(crate) fn context(&self) -> Option<gdk::GLContext> {
|
||||
self.imp().context()
|
||||
}
|
||||
|
||||
pub(crate) fn handle_frame_changed(&self, frame: Option<Frame>) {
|
||||
self.imp().handle_frame_changed(frame);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue