phonto 0.3.1

GPU-accelerated video wallpaper program for wayland compositors & MacOS
use gst::MessageView::*;
use std::path::Path;
use std::sync::mpsc::SyncSender;

use anyhow::{Context, anyhow};
use gstreamer as gst;
use gstreamer::prelude::*;
use gstreamer_app as gst_app;
use gstreamer_gl as gst_gl;
use gstreamer_gl::prelude::*;
use gstreamer_gl_egl as gst_gl_egl;
use gstreamer_video as gst_video;

pub struct Frame {
    pub texture_id: u32,
    pub width: u32,
    pub height: u32,
}

pub fn run(
    path: &Path,
    gl_display: gst_gl::GLDisplay,
    gl_context: gst_gl::GLContext,
    tx: SyncSender<gst::Sample>,
) -> anyhow::Result<()> {
    let pipeline_desc = format!(
        "filesrc location=\"{}\" ! decodebin3 ! glupload ! glcolorconvert ! \
         appsink name=sink caps=video/x-raw(memory:GLMemory),format=RGBA,texture-target=2D \
         sync=true max-buffers=1",
        path.display()
    );

    let pipeline = gst::parse::launch(&pipeline_desc)
        .context("parse pipeline")?
        .downcast::<gst::Pipeline>()
        .map_err(|_| anyhow!("parsed element is not a Pipeline"))?;

    let mut display_ctx = gst::Context::new("gst.gl.GLDisplay", true);
    display_ctx
        .get_mut()
        .expect("freshly created Context has a unique reference")
        .set_gl_display(&gl_display);

    pipeline.set_context(&display_ctx);

    let mut app_ctx = gst::Context::new("gst.gl.app_context", true);
    app_ctx
        .get_mut()
        .expect("freshly created Context has a unique reference")
        .structure_mut()
        .set("context", &gl_context);

    pipeline.set_context(&app_ctx);

    let appsink = pipeline
        .by_name("sink")
        .context("appsink not found in pipeline")?
        .downcast::<gst_app::AppSink>()
        .map_err(|_| anyhow!("`sink` is not an AppSink"))?;

    appsink.set_callbacks(
        gst_app::AppSinkCallbacks::builder()
            .new_sample(move |sink| {
                let sample = sink.pull_sample().map_err(|_| gst::FlowError::Eos)?;
                tx.send(sample).map_err(|_| gst::FlowError::Eos)?;
                Ok(gst::FlowSuccess::Ok)
            })
            .build(),
    );

    pipeline
        .set_state(gst::State::Playing)
        .context("set pipeline state to Playing")?;

    let bus = pipeline.bus().context("pipeline has no bus")?;
    for msg in bus.iter_timed(gst::ClockTime::NONE) {
        match msg.view() {
            Eos(_) => {
                pipeline
                    .seek_simple(
                        gst::SeekFlags::FLUSH | gst::SeekFlags::KEY_UNIT,
                        gst::ClockTime::ZERO,
                    )
                    .context("seek to loop")?;
            }
            Error(err) => {
                pipeline.set_state(gst::State::Null).ok();
                return Err(anyhow!(
                    "pipeline error from {:?}: {} ({:?})",
                    err.src().map(|s| s.path_string()),
                    err.error(),
                    err.debug()
                ));
            }
            _ => {}
        }
    }

    pipeline.set_state(gst::State::Null).ok();
    Ok(())
}

pub fn sample_to_frame(
    sample: gst::Sample,
    gl_context: &gst_gl::GLContext,
) -> anyhow::Result<Frame> {
    let buffer = sample.buffer_owned().context("sample has no buffer")?;
    let caps = sample.caps().context("sample has no caps")?;
    let info = gst_video::VideoInfo::from_caps(caps).context("VideoInfo from caps")?;

    if let Some(sync_meta) = buffer.meta::<gst_gl::GLSyncMeta>() {
        sync_meta.wait(gl_context);
    }

    let gl_frame = gst_gl::GLVideoFrame::from_buffer_readable(buffer, &info)
        .map_err(|_| anyhow!("GLVideoFrame::from_buffer_readable failed"))?;

    let texture_id = gl_frame
        .texture_id(0)
        .context("GLVideoFrame texture id missing")?;

    Ok(Frame {
        texture_id,
        width: info.width(),
        height: info.height(),
    })
}

pub fn wrap_gl(
    egl_display: usize,
    egl_context: usize,
) -> anyhow::Result<(gst_gl::GLDisplay, gst_gl::GLContext)> {
    gst::init().context("gst::init")?;

    let gl_display = unsafe { gst_gl_egl::GLDisplayEGL::with_egl_display(egl_display) }
        .context("wrap EGL display for GStreamer")?;

    let gl_display = gl_display.upcast::<gst_gl::GLDisplay>();

    let gl_context = unsafe {
        gst_gl::GLContext::new_wrapped(
            &gl_display,
            egl_context,
            gst_gl::GLPlatform::EGL,
            gst_gl::GLAPI::GLES2,
        )
    }
    .context("wrap EGL context for GStreamer")?;

    gl_context
        .activate(true)
        .map_err(|_| anyhow!("GLContext::activate failed"))?;

    gl_context
        .fill_info()
        .map_err(|e| anyhow!("GLContext::fill_info: {e}"))?;

    Ok((gl_display, gl_context))
}