Commit 9c6d4775 authored by Guido Gunther's avatar Guido Gunther
Browse files

Initial commit

parents
This diff is collapsed.
µ-Player
========
Minimalistic video player using GTK4 and GStreamer. The main purpose is to make
playing hw accelerated videos with hantro and OpenGL simple.
It supports:
- Inhibiting suspend/idle when playing video
- Stopping video playback on (i.e. power button toggled) screen lock
- Registering as default video player in GNOME control center
![Playing video in landscape fullscreen mode](screenshots/landscape-fullscreen.png)
Building
========
Most distribution don't ship recent enough GTK4 and gstreamer atm so it's simplest
to build and install as a flatpak via
flatpak-builder --force-clean --install --user _build/ org.sigxcpu.Livi.json
#!/usr/bin/env python3
from os import environ, path
from subprocess import call
prefix = environ.get('MESON_INSTALL_PREFIX', '/usr/local')
datadir = path.join(prefix, 'share')
destdir = environ.get('DESTDIR', '')
# Package managers set this so we don't need to run
if not destdir:
print('Updating icon cache...')
call(['gtk-update-icon-cache', '-qtf', path.join(datadir, 'icons', 'hicolor')])
print('Updating desktop database...')
call(['update-desktop-database', '-q', path.join(datadir, 'applications')])
print('Compiling GSettings schemas...')
call(['glib-compile-schemas', path.join(datadir, 'glib-2.0', 'schemas')])
datadir = get_option('datadir')
desktop_file = i18n.merge_file(
input: 'org.sigxcpu.Livi.desktop.in',
output: 'org.sigxcpu.Livi.desktop',
type: 'desktop',
po_dir: '../po',
install: true,
install_dir: join_paths(datadir, 'applications')
)
desktop_utils = find_program('desktop-file-validate', required: false)
if desktop_utils.found()
test('Validate desktop file', desktop_utils,
args: [desktop_file]
)
endif
appstream_file = i18n.merge_file(
input: 'org.sigxcpu.Livi.appdata.xml.in',
output: 'org.sigxcpu.Livi.appdata.xml',
po_dir: '../po',
install: true,
install_dir: join_paths(get_option('datadir'), 'appdata')
)
appstream_util = find_program('appstream-util', required: false)
if appstream_util.found()
test('Validate appstream file', appstream_util,
args: ['validate', appstream_file]
)
endif
install_data('org.sigxcpu.Livi.gschema.xml',
install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas')
)
compile_schemas = find_program('glib-compile-schemas', required: false)
if compile_schemas.found()
test('Validate schema file', compile_schemas,
args: ['--strict', '--dry-run', meson.current_source_dir()]
)
endif
install_data('org.sigxcpu.Livi.svg',
install_dir: join_paths(
datadir,
'icons',
'hicolor',
'scalable',
'apps'
)
)
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>org.sigxcpu.Livi.desktop</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-or-later</project_license>
<description>
<p>
A simple GTK4 based video player
</p>
</description>
<url type="homepage">https://source.puri.sm/guido.gunther/livi</url>
<url type="bugtracker">https://source.puri.sm/guido.gunther/livi/issues</url>
<project_group>GNOME</project_group>
<developer_name>Guido Günther</developer_name>
<recommends>
<control>touch</control>
</recommends>
<categories>
<category>Video</category>
</categories>
<custom>
<value key="Purism::form_factor">workstation</value>
<value key="Purism::form_factor">mobile</value>
</custom>
</component>
[Desktop Entry]
Name=µ-Player
Keywords=Video;Movie;
Comment=Play videos
Exec=livi %U
Terminal=false
Type=Application
Categories=GTK;GNOME;AudioVideo;Player;Video;
StartupNotify=true
MimeType=video/mp4;video/x-ogm+ogg;video/x-matroska;
Icon=org.sigxcpu.Livi
X-GNOME-AutoRestart=true
X-Purism-FormFactor=Workstation;Mobile;
<?xml version="1.0" encoding="UTF-8"?>
<schemalist gettext-domain="livi">
<schema id="org.sigxcpu.Livi" path="/org/sigxcpu/Livi/">
</schema>
</schemalist>
This diff is collapsed.
project('livi', 'c',
version: '0.0.0',
meson_version: '>= 0.50.0',
default_options: [ 'warning_level=2',
'c_std=gnu11',
],
)
i18n = import('i18n')
cc = meson.get_compiler('c')
config_h = configuration_data()
config_h.set_quoted('PACKAGE_VERSION', meson.project_version())
config_h.set_quoted('GETTEXT_PACKAGE', 'livi')
config_h.set_quoted('LOCALEDIR', join_paths(get_option('prefix'), get_option('localedir')))
configure_file(
output: 'livi-config.h',
configuration: config_h,
)
global_c_args = [ '-I' + meson.build_root() ]
test_c_args = [
'-Wcast-align',
'-Wdate-time',
'-Wdeclaration-after-statement',
['-Werror=format-security', '-Werror=format=2'],
'-Wendif-labels',
'-Werror=incompatible-pointer-types',
'-Werror=missing-declarations',
'-Werror=overflow',
'-Werror=return-type',
'-Werror=shift-count-overflow',
'-Werror=shift-overflow=2',
'-Werror=implicit-fallthrough=3',
'-Wfloat-equal',
'-Wformat-nonliteral',
'-Wformat-security',
'-Winit-self',
'-Wmaybe-uninitialized',
'-Wmissing-field-initializers',
'-Wmissing-include-dirs',
'-Wmissing-noreturn',
'-Wnested-externs',
'-Wno-missing-field-initializers',
'-Wno-sign-compare',
'-Wno-strict-aliasing',
'-Wno-unused-parameter',
'-Wold-style-definition',
'-Wpointer-arith',
'-Wredundant-decls',
'-Wshadow',
'-Wstrict-prototypes',
'-Wswitch-default',
'-Wswitch-enum',
'-Wtype-limits',
'-Wundef',
'-Wunused-function',
]
if get_option('buildtype') != 'plain'
test_c_args += '-fstack-protector-strong'
endif
foreach arg: test_c_args
if cc.has_multi_arguments(arg)
global_c_args += arg
endif
endforeach
add_project_arguments(
global_c_args,
language: 'c'
)
subdir('data')
subdir('src')
subdir('po')
meson.add_install_script('build-aux/meson/postinstall.py')
{
"app-id" : "org.sigxcpu.Livi",
"runtime" : "org.gnome.Platform",
"runtime-version" : "40",
"sdk" : "org.gnome.Sdk",
"command" : "livi",
"finish-args" : [
"--device=all",
"--share=ipc",
"--share=network",
"--socket=fallback-x11",
"--socket=pulseaudio",
"--socket=wayland"
],
"cleanup" : [
"/include",
"/lib/pkgconfig",
"/man",
"/share/doc",
"/share/gtk-doc",
"/share/man",
"/share/pkgconfig",
"*.la",
"*.a"
],
"build-options": {
"env": {
"GST_PLUGIN_SYSTEM_PATH": "/app/lib/gstreamer-1.0/"
}
},
"modules" : [
{
"name": "x264",
"config-opts": [
"--enable-shared",
"--enable-static",
"--enable-pic",
"--disable-lavf"
],
"sources": [
{
"type": "archive",
"url": "http://download.videolan.org/pub/x264/snapshots/x264-snapshot-20191103-2245-stable.tar.bz2",
"sha256": "d74f8f819104a94b0bd075aae39652b1957bcdd460e09b5202582c86e95a5bc0"
}
]
},
{
"name" : "gudev",
"buildsystem" : "meson",
"config-opts" : [
"-Dtests=disabled",
"-Dintrospection=disabled"
],
"sources" : [
{
"type" : "git",
"url" : "https://gitlab.gnome.org/GNOME/libgudev.git"
}
]
},
{
"name": "gstreamer",
"buildsystem": "meson",
"builddir": true,
"config-opts": [
"-Ddoc=disabled",
"--libdir=lib"
],
"sources": [
{
"type": "git",
"branch": "master",
"url": "https://gitlab.freedesktop.org/gstreamer/gstreamer.git"
}
]
},
{
"name": "gst-plugins-base",
"buildsystem": "meson",
"config-opts": [
"-Ddoc=disabled",
"-Dorc=enabled",
"--libdir=lib",
"--wrap-mode=nodownload"
],
"builddir": true,
"sources": [
{
"type": "git",
"branch": "master",
"url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-base.git"
}
]
},
{
"name": "gst-plugins-good",
"buildsystem": "meson",
"builddir": true,
"config-opts": [
"-Ddoc=disabled",
"--libdir=lib"
],
"sources": [
{
"type": "git",
"branch": "master",
"url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-good.git"
}
]
},
{
"name": "gst-plugins-bad",
"buildsystem": "meson",
"builddir": true,
"config-opts": [
"-Daom=disabled",
"-Ddoc=disabled",
"-Dv4l2codecs=enabled",
"--libdir=lib"
],
"sources": [
{
"type": "git",
"branch": "master",
"url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad.git"
}
]
},
{
"name" : "livi",
"builddir" : true,
"buildsystem" : "meson",
"sources" : [
{
"type" : "git",
"url" : ".",
"branch" : "main"
}
]
}
]
}
data/org.sigxcpu.Livi.desktop.in
data/org.sigxcpu.Livi.appdata.xml.in
data/org.sigxcpu.Livi.gschema.xml
src/livi-window.ui
src/main.c
src/livi-window.c
i18n.gettext('livi', preset: 'glib')
/*
* Copyright © 2018 Benjamin Otte
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
* Authors: Benjamin Otte <otte@gnome.org>
*/
#include "livi-gst-paintable.h"
#include "livi-gst-sink.h"
#include <gtk/gtk.h>
#include <gst/player/gstplayer-video-renderer.h>
#include <gsk/gl/gskglrenderer.h>
#include <math.h>
struct _GtkGstPaintable
{
GObject parent_instance;
GdkPaintable *image;
double pixel_aspect_ratio;
GdkGLContext *context;
};
struct _GtkGstPaintableClass
{
GObjectClass parent_class;
};
static void
gtk_gst_paintable_paintable_snapshot (GdkPaintable *paintable,
GdkSnapshot *snapshot,
double width,
double height)
{
GtkGstPaintable *self = GTK_GST_PAINTABLE (paintable);
if (self->image)
gdk_paintable_snapshot (self->image, snapshot, width, height);
}
static GdkPaintable *
gtk_gst_paintable_paintable_get_current_image (GdkPaintable *paintable)
{
GtkGstPaintable *self = GTK_GST_PAINTABLE (paintable);
if (self->image)
return GDK_PAINTABLE (g_object_ref (self->image));
return gdk_paintable_new_empty (0, 0);
}
static int
gtk_gst_paintable_paintable_get_intrinsic_width (GdkPaintable *paintable)
{
GtkGstPaintable *self = GTK_GST_PAINTABLE (paintable);
if (self->image)
return round (self->pixel_aspect_ratio *
gdk_paintable_get_intrinsic_width (self->image));
return 0;
}
static int
gtk_gst_paintable_paintable_get_intrinsic_height (GdkPaintable *paintable)
{
GtkGstPaintable *self = GTK_GST_PAINTABLE (paintable);
if (self->image)
return gdk_paintable_get_intrinsic_height (self->image);
return 0;
}
static double
gtk_gst_paintable_paintable_get_intrinsic_aspect_ratio (GdkPaintable *paintable)
{
GtkGstPaintable *self = GTK_GST_PAINTABLE (paintable);
if (self->image)
return self->pixel_aspect_ratio *
gdk_paintable_get_intrinsic_aspect_ratio (self->image);
return 0.0;
};
static void
gtk_gst_paintable_paintable_init (GdkPaintableInterface *iface)
{
iface->snapshot = gtk_gst_paintable_paintable_snapshot;
iface->get_current_image = gtk_gst_paintable_paintable_get_current_image;
iface->get_intrinsic_width = gtk_gst_paintable_paintable_get_intrinsic_width;
iface->get_intrinsic_height = gtk_gst_paintable_paintable_get_intrinsic_height;
iface->get_intrinsic_aspect_ratio = gtk_gst_paintable_paintable_get_intrinsic_aspect_ratio;
}
static GstElement *
gtk_gst_paintable_video_renderer_create_video_sink (GstPlayerVideoRenderer *renderer,
GstPlayer *player)
{
GtkGstPaintable *self = GTK_GST_PAINTABLE (renderer);
GstElement *sink, *glsinkbin;
GdkGLContext *ctx;
sink = g_object_new (GTK_TYPE_GST_SINK,
"paintable", self,
"gl-context", self->context,
NULL);
g_object_get (GTK_GST_SINK (sink), "gl-context", &ctx, NULL);
/* GL rendering won't work otherwise */
g_assert (self->context != NULL && ctx != NULL);
glsinkbin = gst_element_factory_make ("glsinkbin", NULL);
g_object_set (glsinkbin, "sink", sink, NULL);
g_object_unref (ctx);
g_debug ("created gl sink");
return glsinkbin;
}
static void
gtk_gst_paintable_video_renderer_init (GstPlayerVideoRendererInterface *iface)
{
iface->create_video_sink = gtk_gst_paintable_video_renderer_create_video_sink;
}
G_DEFINE_TYPE_WITH_CODE (GtkGstPaintable, gtk_gst_paintable, G_TYPE_OBJECT,
G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE,
gtk_gst_paintable_paintable_init)
G_IMPLEMENT_INTERFACE (GST_TYPE_PLAYER_VIDEO_RENDERER,
gtk_gst_paintable_video_renderer_init));
static void
gtk_gst_paintable_dispose (GObject *object)
{
GtkGstPaintable *self = GTK_GST_PAINTABLE (object);
g_clear_object (&self->image);
G_OBJECT_CLASS (gtk_gst_paintable_parent_class)->dispose (object);
}
static void
gtk_gst_paintable_class_init (GtkGstPaintableClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->dispose = gtk_gst_paintable_dispose;
}
static void
gtk_gst_paintable_init (GtkGstPaintable *self)
{
}
GdkPaintable *
gtk_gst_paintable_new (void)
{
return g_object_new (GTK_TYPE_GST_PAINTABLE, NULL);
}
void
gtk_gst_paintable_realize (GtkGstPaintable *self,
GdkSurface *surface)
{
GError *error = NULL;
if (self->context)
return;
self->context = gdk_surface_create_gl_context (surface, &error);
if (self->context == NULL)
{
GST_INFO ("failed to create GDK GL context: %s", error->message);
g_error_free (error);
return;
}
if (!gdk_gl_context_realize (self->context, &error))
{
GST_INFO ("failed to realize GDK GL context: %s", error->message);
g_clear_object (&self->context);
g_error_free (error);
return;
}
}
void
gtk_gst_paintable_unrealize (GtkGstPaintable *self,
GdkSurface *surface)
{
/* XXX: We could be smarter here and:
* - track how often we were realized with that surface
* - track alternate surfaces
*/
if (self->context == NULL)
return;
if (gdk_gl_context_get_surface (self->context) == surface)
g_clear_object (&self->context);
}
static void
gtk_gst_paintable_set_paintable (GtkGstPaintable *self,
GdkPaintable *paintable,
double pixel_aspect_ratio)
{
gboolean size_changed;
if (self->image == paintable)
return;
if (self->image == NULL ||
self->pixel_aspect_ratio * gdk_paintable_get_intrinsic_width (self->image) !=
pixel_aspect_ratio * gdk_paintable_get_intrinsic_width (paintable) ||
gdk_paintable_get_intrinsic_height (self->image) != gdk_paintable_get_intrinsic_height (paintable) ||
gdk_paintable_get_intrinsic_aspect_ratio (self->image) != gdk_paintable_get_intrinsic_aspect_ratio (paintable))
size_changed = TRUE;
else