mirror of
https://github.com/Rafostar/clapper.git
synced 2025-08-30 16:02:00 +02:00
Compare commits
103 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
4766efbbc4 | ||
|
28c1daf709 | ||
|
aa49d25df5 | ||
|
774687710f | ||
|
901fc8d760 | ||
|
ab32b2dbbc | ||
|
24bb9f298b | ||
|
2efa3e0bf6 | ||
|
92e3f7d93c | ||
|
85804ea297 | ||
|
7cf86e92eb | ||
|
b5711b145b | ||
|
9271392397 | ||
|
175de5bd6d | ||
|
7b97f29aaf | ||
|
8d7fb761f7 | ||
|
aec2166c11 | ||
|
c21b214477 | ||
|
d9939a94c2 | ||
|
dafa2cfdf5 | ||
|
ebe72f20b5 | ||
|
fa1455556b | ||
|
8fb6b971fe | ||
|
1bf46a2f12 | ||
|
a39c67e5e7 | ||
|
a3f78432f8 | ||
|
7a7a04554f | ||
|
93de3dc056 | ||
|
eda80f314e | ||
|
b3e6890571 | ||
|
c767b3e4b2 | ||
|
ec6157763b | ||
|
cca3077936 | ||
|
b5e1b3ab86 | ||
|
b15b94fc90 | ||
|
28d8986072 | ||
|
30a7229b33 | ||
|
9502e062f4 | ||
|
6a9c77dfad | ||
|
133cda1b41 | ||
|
e68a7fe31a | ||
|
7f69bee11c | ||
|
295af9fd24 | ||
|
d04297620b | ||
|
43acfddb06 | ||
|
d54781eda7 | ||
|
96e5c5aa7c | ||
|
66ce006f00 | ||
|
3fd30e41bf | ||
|
a6316c940c | ||
|
84d9cc7416 | ||
|
a3499e9b47 | ||
|
4a60e01131 | ||
|
b404eb2f56 | ||
|
1f18796e0d | ||
|
254aa538a5 | ||
|
58cc45ec7d | ||
|
7a75c6d4ff | ||
|
2e97fc362c | ||
|
d762a59cc4 | ||
|
b42843be1f | ||
|
6dc825dfb3 | ||
|
e89b3599c9 | ||
|
79e12a6e36 | ||
|
36d4a5c848 | ||
|
38e5bae199 | ||
|
fcff4b4450 | ||
|
4021745a56 | ||
|
bd20d305ba | ||
|
d9b35b7fb8 | ||
|
f1e00434ba | ||
|
918157be04 | ||
|
72b55939b4 | ||
|
e0a3ef78db | ||
|
4f46a7eaa8 | ||
|
050ef440dc | ||
|
a4d55f8114 | ||
|
aa60c56a58 | ||
|
8c307dc90f | ||
|
5b6141ee8c | ||
|
8f294604dc | ||
|
06f8e5d259 | ||
|
6370e1126b | ||
|
270e59137d | ||
|
ec18ca989a | ||
|
46d24536c0 | ||
|
c89d488c30 | ||
|
01c26cbbc3 | ||
|
4375077dbc | ||
|
fceb8ff70a | ||
|
6dc37088cf | ||
|
4e85f6b749 | ||
|
0cd82b1b8a | ||
|
39da52dd62 | ||
|
9c12afbf80 | ||
|
e3c9b112e2 | ||
|
13d675beff | ||
|
95c3845398 | ||
|
93549a67af | ||
|
07fb0a9a46 | ||
|
fe3fd32932 | ||
|
637212f7e8 | ||
|
8b2e63ac48 |
4
.gitattributes
vendored
4
.gitattributes
vendored
@@ -1 +1,3 @@
|
||||
lib/* linguist-vendored
|
||||
lib/**/* linguist-vendored
|
||||
lib/**/**/* linguist-vendored
|
||||
lib/gst/clapper/gtk4/* linguist-vendored=false
|
||||
|
12
README.md
12
README.md
@@ -28,9 +28,9 @@ The media player is using [GStreamer](https://gstreamer.freedesktop.org/) as a m
|
||||
The `Flatpak` package includes all required dependencies and codecs.
|
||||
Additionally it also has a few patches, thus some functionalities work better (or are only available) in `Flatpak` version (until my changes are accepted upstream). List of patches used in this version can be found [here](https://github.com/Rafostar/clapper/issues/35).
|
||||
|
||||
```sh
|
||||
flatpak install https://rafostar.github.io/flatpak/com.github.rafostar.Clapper.flatpakref
|
||||
```
|
||||
<a href='https://flathub.org/apps/details/com.github.rafostar.Clapper'><img width='240' alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-en.png'/></a>
|
||||
|
||||
**Important:** If you have been using the flatpak package from my custom 3rd party repo, please remove it and replace your installation with version from Flathub. That repository will not be maintained any longer. Thank you for understanding.
|
||||
|
||||
## Packages
|
||||
The [pkgs folder](https://github.com/Rafostar/clapper/tree/master/pkgs) in this repository contains build scripts for various package formats. You can use them to build package yourself or download one of pre-built packages:
|
||||
@@ -39,7 +39,9 @@ The [pkgs folder](https://github.com/Rafostar/clapper/tree/master/pkgs) in this
|
||||
Pre-built packages are available in [my repo](https://software.opensuse.org//download.html?project=home%3ARafostar&package=clapper) ([see status](https://build.opensuse.org/package/show/home:Rafostar/clapper))
|
||||
|
||||
#### Arch Linux
|
||||
You can get Clapper from the AUR: [clapper-git](https://aur.archlinux.org/packages/clapper-git)
|
||||
You can get Clapper from the AUR:
|
||||
* [clapper](https://aur.archlinux.org/packages/clapper) (stable version)
|
||||
* [clapper-git](https://aur.archlinux.org/packages/clapper-git)
|
||||
|
||||
## Installation from source code
|
||||
```sh
|
||||
@@ -57,7 +59,7 @@ All these libs are acting "on their own" and no function calls from `GJS` relate
|
||||
**Q:** What settings should I set to maximize performance?<br>
|
||||
**A:** As of now, player works best on `Wayland` session. `Wayland` users can try enabling highly experimental `vah264dec` plugin for improved performance (this plugin does not work on `Xorg` right now) for standard (8-bit) `H.264` videos.
|
||||
It can be enabled from inside player preferences dialog inside `Advanced -> GStreamer` tab using customizable `Plugin Ranking` feature.
|
||||
Since the whole app is rendered using your GPU, users of VERY weak GPUs might try to disable the "render window shadows" option to have more GPU power available for non-fullscreen video rendering.
|
||||
Since the whole app is rendered using your GPU, users of VERY weak GPUs might want to disable the "render window shadows" option to have more GPU power available for non-fullscreen video rendering.
|
||||
|
||||
## Other Questions?
|
||||
Feel free to ask me any questions.<br>
|
||||
|
@@ -9,7 +9,7 @@ radio {
|
||||
.osd list {
|
||||
background: none;
|
||||
}
|
||||
.osd list row {
|
||||
.osd list row image {
|
||||
-gtk-icon-shadow: none;
|
||||
}
|
||||
.gtk402 trough highlight {
|
||||
@@ -25,7 +25,7 @@ radio {
|
||||
border: transparent;
|
||||
}
|
||||
.linkseparator {
|
||||
background: alpha(@borders, 0.75);
|
||||
background: rgba(24,24,24,0.72);
|
||||
min-width: 1px;
|
||||
}
|
||||
.linkedleft image {
|
||||
@@ -61,6 +61,9 @@ radio {
|
||||
.roundedcorners {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.adwthemedark scale trough highlight {
|
||||
filter: brightness(120%);
|
||||
}
|
||||
|
||||
.videowidget {
|
||||
min-width: 320px;
|
||||
@@ -72,9 +75,26 @@ radio {
|
||||
font-size: 21px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.tvmode button {
|
||||
.adwicons .playercontrols {
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
.playercontrols {
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
.playercontrols button {
|
||||
margin: 3px;
|
||||
margin-left: 1px;
|
||||
margin-right: 1px;
|
||||
}
|
||||
.tvmode .playercontrols button {
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
margin: 5px;
|
||||
margin-left: 3px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
.tvmode button image {
|
||||
-gtk-icon-shadow: none;
|
||||
}
|
||||
.tvmode radio {
|
||||
@@ -85,27 +105,25 @@ radio {
|
||||
min-height: 17px;
|
||||
}
|
||||
|
||||
.tvmode .playercontrols {
|
||||
.tvmode .playercontrols button image {
|
||||
-gtk-icon-size: 24px;
|
||||
}
|
||||
.playbackicon {
|
||||
.adwicons .playbackicon {
|
||||
-gtk-icon-size: 20px;
|
||||
}
|
||||
.tvmode .playbackicon {
|
||||
.adwicons.tvmode .playbackicon {
|
||||
-gtk-icon-size: 28px;
|
||||
}
|
||||
.labelbutton {
|
||||
.labelbuttonlabel {
|
||||
margin-left: -4px;
|
||||
margin-right: -4px;
|
||||
margin-top: 1px;
|
||||
min-width: 8px;
|
||||
font-family: 'Cantarell', sans-serif;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tvmode .labelbutton {
|
||||
margin-top: 0px;
|
||||
font-size: 23px;
|
||||
.tvmode .labelbuttonlabel {
|
||||
font-size: 22px;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
@@ -143,11 +161,9 @@ radio {
|
||||
|
||||
/* Position Scale */
|
||||
.positionscale {
|
||||
margin-top: -2px;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
.tvmode .positionscale {
|
||||
margin-top: -1px;
|
||||
margin: -2px;
|
||||
margin-left: -4px;
|
||||
margin-right: -4px;
|
||||
}
|
||||
.positionscale trough highlight {
|
||||
min-height: 4px;
|
||||
@@ -301,9 +317,6 @@ radio {
|
||||
.gpufriendlyfs {
|
||||
box-shadow: none;
|
||||
}
|
||||
.brightscale trough highlight {
|
||||
filter: brightness(120%);
|
||||
}
|
||||
|
||||
/* Error BG */
|
||||
.blackbackground {
|
||||
|
@@ -84,10 +84,6 @@
|
||||
<default>true</default>
|
||||
<summary>Enable to force the app to use dark theme variant</summary>
|
||||
</key>
|
||||
<key name="brighter-sliders" type="b">
|
||||
<default>true</default>
|
||||
<summary>Enable to make all sliders/bars brighter</summary>
|
||||
</key>
|
||||
<key name="render-shadows" type="b">
|
||||
<default>true</default>
|
||||
<summary>Enable rendering window shadows (only if theme has them)</summary>
|
||||
@@ -103,9 +99,15 @@
|
||||
<summary>Set PlayFlags for playbin</summary>
|
||||
</key>
|
||||
|
||||
<!-- YouTube -->
|
||||
<key name="yt-adaptive-enabled" type="b">
|
||||
<default>false</default>
|
||||
<summary>Enable to use adaptive streaming</summary>
|
||||
</key>
|
||||
|
||||
<!-- Other -->
|
||||
<key name="window-size" type="s">
|
||||
<default>'[960, 583]'</default>
|
||||
<default>'[800, 490]'</default>
|
||||
<summary>Stores window size to restore on next launch</summary>
|
||||
</key>
|
||||
<key name="volume-last" type="d">
|
||||
|
@@ -52,6 +52,26 @@
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<releases>
|
||||
<release version="0.2.0" date="2021-04-13">
|
||||
<description>
|
||||
<p>New features:</p>
|
||||
<ul>
|
||||
<li>YouTube support - drag and drop videos from youtube or use open URI dialog to play them</li>
|
||||
<li>Added convenient ways of opening external subtitles</li>
|
||||
</ul>
|
||||
<p>Changes:</p>
|
||||
<ul>
|
||||
<li>Few GUI layout improvements</li>
|
||||
<li>Simplified video sink code</li>
|
||||
<li>Fixed missing Ctrl+O common keybinding</li>
|
||||
<li>Fixed error when playback finishes during controls reveal animation</li>
|
||||
<li>Fixed startup window size on Xorg</li>
|
||||
<li>Fixed top time not showing up on fullscreen startup</li>
|
||||
<li>Fixed missing file extensions in online URIs</li>
|
||||
<li>Fixed some error messages not being displayed</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.1.0" date="2021-02-26">
|
||||
<description>
|
||||
<p>First stable release</p>
|
||||
|
28
lib/gst/clapper/gstclapper-gtk4-plugin.c
vendored
28
lib/gst/clapper/gstclapper-gtk4-plugin.c
vendored
@@ -30,7 +30,7 @@
|
||||
#endif
|
||||
|
||||
#include "gstclapper-gtk4-plugin.h"
|
||||
#include "gtk4/gstgtkglsink.h"
|
||||
#include "gtk4/gstclapperglsink.h"
|
||||
|
||||
enum
|
||||
{
|
||||
@@ -77,9 +77,7 @@ gst_clapper_gtk4_plugin_constructed (GObject * object)
|
||||
{
|
||||
GstClapperGtk4Plugin *self = GST_CLAPPER_GTK4_PLUGIN (object);
|
||||
|
||||
if (!self->video_sink)
|
||||
self->video_sink = g_object_new (GST_TYPE_GTK_GL_SINK, NULL);
|
||||
|
||||
self->video_sink = g_object_new (GST_TYPE_CLAPPER_GL_SINK, NULL);
|
||||
gst_object_ref_sink (self->video_sink);
|
||||
|
||||
G_OBJECT_CLASS (parent_class)->constructed (object);
|
||||
@@ -111,35 +109,15 @@ gst_clapper_gtk4_plugin_finalize (GObject * object)
|
||||
G_OBJECT_CLASS (parent_class)->finalize (object);
|
||||
}
|
||||
|
||||
#define C_ENUM(v) ((gint) v)
|
||||
|
||||
GType
|
||||
gst_clapper_gtk4_plugin_type_get_type (void)
|
||||
{
|
||||
static gsize id = 0;
|
||||
static const GEnumValue values[] = {
|
||||
{C_ENUM (GST_CLAPPER_GTK4_PLUGIN_TYPE_GLAREA), "GST_CLAPPER_GTK4_PLUGIN_TYPE_GLAREA", "glarea"},
|
||||
{0, NULL, NULL}
|
||||
};
|
||||
|
||||
if (g_once_init_enter (&id)) {
|
||||
GType tmp = g_enum_register_static ("GstClapperGtk4PluginType", values);
|
||||
g_once_init_leave (&id, tmp);
|
||||
}
|
||||
|
||||
return (GType) id;
|
||||
}
|
||||
|
||||
/**
|
||||
* gst_clapper_gtk4_plugin_new:
|
||||
* @plugin_type: (allow-none): Requested GstClapperGtk4PluginType
|
||||
*
|
||||
* Creates a new GTK4 plugin.
|
||||
*
|
||||
* Returns: (transfer full): the new GstClapperGtk4Plugin
|
||||
*/
|
||||
GstClapperGtk4Plugin *
|
||||
gst_clapper_gtk4_plugin_new (G_GNUC_UNUSED const GstClapperGtk4PluginType plugin_type)
|
||||
gst_clapper_gtk4_plugin_new (void)
|
||||
{
|
||||
return g_object_new (GST_TYPE_CLAPPER_GTK4_PLUGIN, NULL);
|
||||
}
|
||||
|
16
lib/gst/clapper/gstclapper-gtk4-plugin.h
vendored
16
lib/gst/clapper/gstclapper-gtk4-plugin.h
vendored
@@ -26,20 +26,6 @@
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
/* PluginType */
|
||||
GST_CLAPPER_API
|
||||
GType gst_clapper_gtk4_plugin_type_get_type (void);
|
||||
#define GST_TYPE_CLAPPER_GTK4_PLUGIN_TYPE (gst_clapper_gtk4_plugin_type_get_type ())
|
||||
|
||||
/**
|
||||
* GstClapperGtk4PluginType:
|
||||
* @GST_CLAPPER_GTK4_PLUGIN_TYPE_GLAREA: GTK4 GLArea sink.
|
||||
*/
|
||||
typedef enum
|
||||
{
|
||||
GST_CLAPPER_GTK4_PLUGIN_TYPE_GLAREA,
|
||||
} GstClapperGtk4PluginType;
|
||||
|
||||
#define GST_TYPE_CLAPPER_GTK4_PLUGIN (gst_clapper_gtk4_plugin_get_type ())
|
||||
#define GST_IS_CLAPPER_GTK4_PLUGIN(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_CLAPPER_GTK4_PLUGIN))
|
||||
#define GST_IS_CLAPPER_GTK4_PLUGIN_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_CLAPPER_GTK4_PLUGIN))
|
||||
@@ -79,7 +65,7 @@ GST_CLAPPER_API
|
||||
GType gst_clapper_gtk4_plugin_get_type (void);
|
||||
|
||||
GST_CLAPPER_API
|
||||
GstClapperGtk4Plugin * gst_clapper_gtk4_plugin_new (const GstClapperGtk4PluginType plugin_type);
|
||||
GstClapperGtk4Plugin * gst_clapper_gtk4_plugin_new (void);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
|
90
lib/gst/clapper/gstclapper.c
vendored
90
lib/gst/clapper/gstclapper.c
vendored
@@ -106,6 +106,7 @@ enum
|
||||
SIGNAL_ERROR,
|
||||
SIGNAL_WARNING,
|
||||
SIGNAL_VIDEO_DIMENSIONS_CHANGED,
|
||||
SIGNAL_MEDIA_INFO_UPDATED,
|
||||
SIGNAL_MUTE_CHANGED,
|
||||
SIGNAL_LAST
|
||||
};
|
||||
@@ -168,6 +169,9 @@ struct _GstClapper
|
||||
* is emitted after gst_clapper_stop/pause() has been called by the user. */
|
||||
gboolean inhibit_sigs;
|
||||
|
||||
/* If should emit media info updated signal */
|
||||
gboolean needs_info_update;
|
||||
|
||||
/* For playbin3 */
|
||||
gboolean use_playbin3;
|
||||
GstStreamCollection *collection;
|
||||
@@ -263,6 +267,7 @@ gst_clapper_init (GstClapper * self)
|
||||
self->seek_position = GST_CLOCK_TIME_NONE;
|
||||
self->last_seek_time = GST_CLOCK_TIME_NONE;
|
||||
self->inhibit_sigs = FALSE;
|
||||
self->needs_info_update = FALSE;
|
||||
|
||||
GST_TRACE_OBJECT (self, "Initialized");
|
||||
}
|
||||
@@ -421,6 +426,11 @@ gst_clapper_class_init (GstClapperClass * klass)
|
||||
G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, 0, NULL,
|
||||
NULL, NULL, G_TYPE_NONE, 1, G_TYPE_ERROR);
|
||||
|
||||
signals[SIGNAL_MEDIA_INFO_UPDATED] =
|
||||
g_signal_new ("media-info-updated", G_TYPE_FROM_CLASS (klass),
|
||||
G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, 0, NULL,
|
||||
NULL, NULL, G_TYPE_NONE, 1, GST_TYPE_CLAPPER_MEDIA_INFO);
|
||||
|
||||
signals[SIGNAL_VIDEO_DIMENSIONS_CHANGED] =
|
||||
g_signal_new ("video-dimensions-changed", G_TYPE_FROM_CLASS (klass),
|
||||
G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, 0, NULL,
|
||||
@@ -838,6 +848,49 @@ main_loop_running_cb (gpointer user_data)
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
typedef struct
|
||||
{
|
||||
GstClapper *clapper;
|
||||
GstClapperMediaInfo *info;
|
||||
} MediaInfoUpdatedSignalData;
|
||||
|
||||
static void
|
||||
media_info_updated_dispatch (gpointer user_data)
|
||||
{
|
||||
MediaInfoUpdatedSignalData *data = user_data;
|
||||
|
||||
if (data->clapper->inhibit_sigs)
|
||||
return;
|
||||
|
||||
if (data->clapper->target_state >= GST_STATE_PAUSED) {
|
||||
g_signal_emit (data->clapper, signals[SIGNAL_MEDIA_INFO_UPDATED], 0,
|
||||
data->info);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
free_media_info_updated_signal_data (MediaInfoUpdatedSignalData * data)
|
||||
{
|
||||
g_object_unref (data->clapper);
|
||||
g_object_unref (data->info);
|
||||
g_free (data);
|
||||
}
|
||||
|
||||
static void
|
||||
emit_media_info_updated (GstClapper * self)
|
||||
{
|
||||
MediaInfoUpdatedSignalData *data = g_new (MediaInfoUpdatedSignalData, 1);
|
||||
data->clapper = g_object_ref (self);
|
||||
g_mutex_lock (&self->lock);
|
||||
self->needs_info_update = FALSE;
|
||||
data->info = gst_clapper_media_info_copy (self->media_info);
|
||||
g_mutex_unlock (&self->lock);
|
||||
|
||||
gst_clapper_signal_dispatcher_dispatch (self->signal_dispatcher, self,
|
||||
media_info_updated_dispatch, data,
|
||||
(GDestroyNotify) free_media_info_updated_signal_data);
|
||||
}
|
||||
|
||||
typedef struct
|
||||
{
|
||||
GstClapper *clapper;
|
||||
@@ -1468,7 +1521,14 @@ notify_caps_cb (G_GNUC_UNUSED GObject * object,
|
||||
{
|
||||
GstClapper *self = GST_CLAPPER (user_data);
|
||||
|
||||
check_video_dimensions_changed (self);
|
||||
if (self->target_state >= GST_STATE_PAUSED) {
|
||||
check_video_dimensions_changed (self);
|
||||
|
||||
g_mutex_lock (&self->lock);
|
||||
if (self->media_info != NULL)
|
||||
self->needs_info_update = TRUE;
|
||||
g_mutex_unlock (&self->lock);
|
||||
}
|
||||
}
|
||||
|
||||
typedef struct
|
||||
@@ -1568,6 +1628,7 @@ state_changed_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg,
|
||||
} else {
|
||||
self->cached_duration = GST_CLOCK_TIME_NONE;
|
||||
}
|
||||
emit_media_info_updated (self);
|
||||
}
|
||||
|
||||
if (new_state == GST_STATE_PAUSED
|
||||
@@ -2269,6 +2330,7 @@ stream_notify_cb (GstStreamCollection * collection, GstStream * stream,
|
||||
{
|
||||
GstClapperStreamInfo *info;
|
||||
const gchar *stream_id;
|
||||
gboolean emit_update = FALSE;
|
||||
|
||||
if (!self->media_info)
|
||||
return;
|
||||
@@ -2283,7 +2345,11 @@ stream_notify_cb (GstStreamCollection * collection, GstStream * stream,
|
||||
gst_clapper_stream_info_find_from_stream_id (self->media_info, stream_id);
|
||||
if (info)
|
||||
gst_clapper_stream_info_update_from_stream (self, info, stream);
|
||||
emit_update = (self->needs_info_update && GST_IS_CLAPPER_VIDEO_INFO (info));
|
||||
g_mutex_unlock (&self->lock);
|
||||
|
||||
if (emit_update)
|
||||
emit_media_info_updated (self);
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -2672,8 +2738,13 @@ static void
|
||||
video_tags_changed_cb (G_GNUC_UNUSED GstElement * playbin, gint stream_index,
|
||||
gpointer user_data)
|
||||
{
|
||||
tags_changed_cb (GST_CLAPPER (user_data), stream_index,
|
||||
GstClapper *self = GST_CLAPPER (user_data);
|
||||
|
||||
tags_changed_cb (self, stream_index,
|
||||
GST_TYPE_CLAPPER_VIDEO_INFO);
|
||||
|
||||
if (self->needs_info_update)
|
||||
emit_media_info_updated (self);
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -2741,9 +2812,18 @@ mute_notify_cb (G_GNUC_UNUSED GObject * obj, G_GNUC_UNUSED GParamSpec * pspec,
|
||||
}
|
||||
|
||||
static void
|
||||
source_setup_cb (GstElement * playbin, GstElement * source, GstClapper * self)
|
||||
element_setup_cb (GstElement * playbin, GstElement * element, GstClapper * self)
|
||||
{
|
||||
GParamSpec *prop = g_object_class_find_property (G_OBJECT_GET_CLASS (element),
|
||||
"user-agent");
|
||||
|
||||
if (prop && prop->value_type == G_TYPE_STRING) {
|
||||
const gchar *user_agent =
|
||||
"Mozilla/5.0 (X11; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0";
|
||||
|
||||
GST_INFO_OBJECT (self, "Setting element user-agent: %s", user_agent);
|
||||
g_object_set (element, "user-agent", user_agent, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
static gpointer
|
||||
@@ -2855,8 +2935,8 @@ gst_clapper_main (gpointer data)
|
||||
G_CALLBACK (volume_notify_cb), self);
|
||||
g_signal_connect (self->playbin, "notify::mute",
|
||||
G_CALLBACK (mute_notify_cb), self);
|
||||
g_signal_connect (self->playbin, "source-setup",
|
||||
G_CALLBACK (source_setup_cb), self);
|
||||
g_signal_connect (self->playbin, "element-setup",
|
||||
G_CALLBACK (element_setup_cb), self);
|
||||
|
||||
self->target_state = GST_STATE_NULL;
|
||||
self->current_state = GST_STATE_NULL;
|
||||
|
732
lib/gst/clapper/gtk4/gstclapperglsink.c
Normal file
732
lib/gst/clapper/gtk4/gstclapperglsink.c
Normal file
@@ -0,0 +1,732 @@
|
||||
/*
|
||||
* GStreamer
|
||||
* Copyright (C) 2015 Matthew Waters <matthew@centricular.com>
|
||||
* Copyright (C) 2020 Rafał Dzięgiel <rafostar.github@gmail.com>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 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
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
/**
|
||||
* SECTION:gstclapperglsink
|
||||
* @title: GstClapperGLSink
|
||||
*
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include <gst/gl/gstglfuncs.h>
|
||||
|
||||
#include "gstclapperglsink.h"
|
||||
#include "gstgtkutils.h"
|
||||
|
||||
GST_DEBUG_CATEGORY (gst_debug_clapper_gl_sink);
|
||||
#define GST_CAT_DEFAULT gst_debug_clapper_gl_sink
|
||||
|
||||
#define DEFAULT_FORCE_ASPECT_RATIO TRUE
|
||||
#define DEFAULT_PAR_N 0
|
||||
#define DEFAULT_PAR_D 1
|
||||
#define DEFAULT_IGNORE_TEXTURES FALSE
|
||||
|
||||
static GstStaticPadTemplate gst_clapper_gl_sink_template =
|
||||
GST_STATIC_PAD_TEMPLATE ("sink",
|
||||
GST_PAD_SINK,
|
||||
GST_PAD_ALWAYS,
|
||||
GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE_WITH_FEATURES
|
||||
(GST_CAPS_FEATURE_MEMORY_GL_MEMORY, "RGBA") "; "
|
||||
GST_VIDEO_CAPS_MAKE_WITH_FEATURES
|
||||
(GST_CAPS_FEATURE_MEMORY_GL_MEMORY ", "
|
||||
GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION, "RGBA")));
|
||||
|
||||
static void gst_clapper_gl_sink_finalize (GObject * object);
|
||||
static void gst_clapper_gl_sink_set_property (GObject * object, guint prop_id,
|
||||
const GValue * value, GParamSpec * param_spec);
|
||||
static void gst_clapper_gl_sink_get_property (GObject * object, guint prop_id,
|
||||
GValue * value, GParamSpec * param_spec);
|
||||
|
||||
static gboolean gst_clapper_gl_sink_propose_allocation (GstBaseSink * bsink,
|
||||
GstQuery * query);
|
||||
static gboolean gst_clapper_gl_sink_query (GstBaseSink * bsink, GstQuery * query);
|
||||
static gboolean gst_clapper_gl_sink_start (GstBaseSink * bsink);
|
||||
static gboolean gst_clapper_gl_sink_stop (GstBaseSink * bsink);
|
||||
|
||||
static GstStateChangeReturn
|
||||
gst_clapper_gl_sink_change_state (GstElement * element,
|
||||
GstStateChange transition);
|
||||
|
||||
static void gst_clapper_gl_sink_get_times (GstBaseSink * bsink, GstBuffer * buf,
|
||||
GstClockTime * start, GstClockTime * end);
|
||||
static GstCaps *gst_clapper_gl_sink_get_caps (GstBaseSink * bsink,
|
||||
GstCaps * filter);
|
||||
static gboolean gst_clapper_gl_sink_set_caps (GstBaseSink * bsink,
|
||||
GstCaps * caps);
|
||||
static GstFlowReturn gst_clapper_gl_sink_show_frame (GstVideoSink * bsink,
|
||||
GstBuffer * buf);
|
||||
|
||||
static void
|
||||
gst_clapper_gl_sink_navigation_interface_init (GstNavigationInterface * iface);
|
||||
|
||||
enum
|
||||
{
|
||||
PROP_0,
|
||||
PROP_WIDGET,
|
||||
PROP_FORCE_ASPECT_RATIO,
|
||||
PROP_PIXEL_ASPECT_RATIO,
|
||||
PROP_IGNORE_TEXTURES,
|
||||
};
|
||||
|
||||
#define gst_clapper_gl_sink_parent_class parent_class
|
||||
G_DEFINE_TYPE_WITH_CODE (GstClapperGLSink, gst_clapper_gl_sink,
|
||||
GST_TYPE_VIDEO_SINK,
|
||||
G_IMPLEMENT_INTERFACE (GST_TYPE_NAVIGATION,
|
||||
gst_clapper_gl_sink_navigation_interface_init);
|
||||
GST_DEBUG_CATEGORY_INIT (gst_debug_clapper_gl_sink,
|
||||
"clapperglsink", 0, "Clapper GL Sink"));
|
||||
|
||||
static void
|
||||
gst_clapper_gl_sink_class_init (GstClapperGLSinkClass * klass)
|
||||
{
|
||||
GObjectClass *gobject_class;
|
||||
GstElementClass *gstelement_class;
|
||||
GstBaseSinkClass *gstbasesink_class;
|
||||
GstVideoSinkClass *gstvideosink_class;
|
||||
GstClapperGLSinkClass *gstclapperglsink_class;
|
||||
|
||||
gobject_class = (GObjectClass *) klass;
|
||||
gstelement_class = (GstElementClass *) klass;
|
||||
gstbasesink_class = (GstBaseSinkClass *) klass;
|
||||
gstvideosink_class = (GstVideoSinkClass *) klass;
|
||||
gstclapperglsink_class = (GstClapperGLSinkClass *) klass;
|
||||
|
||||
gobject_class->set_property = gst_clapper_gl_sink_set_property;
|
||||
gobject_class->get_property = gst_clapper_gl_sink_get_property;
|
||||
|
||||
g_object_class_install_property (gobject_class, PROP_WIDGET,
|
||||
g_param_spec_object ("widget", "GTK Widget",
|
||||
"The GtkWidget to place in the widget hierarchy "
|
||||
"(must only be get from the GTK main thread)",
|
||||
GTK_TYPE_WIDGET, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
|
||||
|
||||
g_object_class_install_property (gobject_class, PROP_FORCE_ASPECT_RATIO,
|
||||
g_param_spec_boolean ("force-aspect-ratio",
|
||||
"Force aspect ratio",
|
||||
"When enabled, scaling will respect original aspect ratio",
|
||||
DEFAULT_FORCE_ASPECT_RATIO,
|
||||
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
||||
|
||||
g_object_class_install_property (gobject_class, PROP_PIXEL_ASPECT_RATIO,
|
||||
gst_param_spec_fraction ("pixel-aspect-ratio", "Pixel Aspect Ratio",
|
||||
"The pixel aspect ratio of the device", DEFAULT_PAR_N, DEFAULT_PAR_D,
|
||||
G_MAXINT, 1, 1, 1, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
||||
|
||||
g_object_class_install_property (gobject_class, PROP_IGNORE_TEXTURES,
|
||||
g_param_spec_boolean ("ignore-textures", "Ignore Textures",
|
||||
"When enabled, textures will be ignored and not drawn",
|
||||
DEFAULT_IGNORE_TEXTURES, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
||||
|
||||
gobject_class->finalize = gst_clapper_gl_sink_finalize;
|
||||
|
||||
gstelement_class->change_state = gst_clapper_gl_sink_change_state;
|
||||
|
||||
gstbasesink_class->get_caps = gst_clapper_gl_sink_get_caps;
|
||||
gstbasesink_class->set_caps = gst_clapper_gl_sink_set_caps;
|
||||
gstbasesink_class->get_times = gst_clapper_gl_sink_get_times;
|
||||
gstbasesink_class->propose_allocation = gst_clapper_gl_sink_propose_allocation;
|
||||
gstbasesink_class->query = gst_clapper_gl_sink_query;
|
||||
gstbasesink_class->start = gst_clapper_gl_sink_start;
|
||||
gstbasesink_class->stop = gst_clapper_gl_sink_stop;
|
||||
|
||||
gstvideosink_class->show_frame = gst_clapper_gl_sink_show_frame;
|
||||
|
||||
gstclapperglsink_class->create_widget = gtk_clapper_gl_widget_new;
|
||||
gstclapperglsink_class->window_title = "GTK4 GL Renderer";
|
||||
|
||||
gst_element_class_set_metadata (gstelement_class,
|
||||
"GTK4 GL Video Sink",
|
||||
"Sink/Video", "A video sink that renders to a GtkWidget using OpenGL",
|
||||
"Matthew Waters <matthew@centricular.com>, "
|
||||
"Rafał Dzięgiel <rafostar.github@gmail.com>");
|
||||
|
||||
gst_element_class_add_static_pad_template (gstelement_class,
|
||||
&gst_clapper_gl_sink_template);
|
||||
|
||||
gst_type_mark_as_plugin_api (GST_TYPE_CLAPPER_GL_SINK, 0);
|
||||
}
|
||||
|
||||
static void
|
||||
gst_clapper_gl_sink_init (GstClapperGLSink * clapper_sink)
|
||||
{
|
||||
clapper_sink->force_aspect_ratio = DEFAULT_FORCE_ASPECT_RATIO;
|
||||
clapper_sink->par_n = DEFAULT_PAR_N;
|
||||
clapper_sink->par_d = DEFAULT_PAR_D;
|
||||
clapper_sink->ignore_textures = DEFAULT_IGNORE_TEXTURES;
|
||||
}
|
||||
|
||||
static void
|
||||
gst_clapper_gl_sink_finalize (GObject * object)
|
||||
{
|
||||
GstClapperGLSink *clapper_sink = GST_CLAPPER_GL_SINK (object);
|
||||
|
||||
GST_DEBUG ("Finalizing Clapper GL sink");
|
||||
|
||||
GST_OBJECT_LOCK (clapper_sink);
|
||||
if (clapper_sink->window && clapper_sink->window_destroy_id)
|
||||
g_signal_handler_disconnect (clapper_sink->window, clapper_sink->window_destroy_id);
|
||||
if (clapper_sink->widget && clapper_sink->widget_destroy_id)
|
||||
g_signal_handler_disconnect (clapper_sink->widget, clapper_sink->widget_destroy_id);
|
||||
|
||||
g_clear_object (&clapper_sink->widget);
|
||||
GST_OBJECT_UNLOCK (clapper_sink);
|
||||
|
||||
G_OBJECT_CLASS (parent_class)->finalize (object);
|
||||
}
|
||||
|
||||
static void
|
||||
widget_destroy_cb (GtkWidget * widget, GstClapperGLSink * clapper_sink)
|
||||
{
|
||||
GST_OBJECT_LOCK (clapper_sink);
|
||||
g_clear_object (&clapper_sink->widget);
|
||||
GST_OBJECT_UNLOCK (clapper_sink);
|
||||
}
|
||||
|
||||
static void
|
||||
window_destroy_cb (GtkWidget * widget, GstClapperGLSink * clapper_sink)
|
||||
{
|
||||
GST_OBJECT_LOCK (clapper_sink);
|
||||
if (clapper_sink->widget) {
|
||||
if (clapper_sink->widget_destroy_id) {
|
||||
g_signal_handler_disconnect (clapper_sink->widget,
|
||||
clapper_sink->widget_destroy_id);
|
||||
clapper_sink->widget_destroy_id = 0;
|
||||
}
|
||||
g_clear_object (&clapper_sink->widget);
|
||||
}
|
||||
clapper_sink->window = NULL;
|
||||
GST_OBJECT_UNLOCK (clapper_sink);
|
||||
}
|
||||
|
||||
static GtkClapperGLWidget *
|
||||
gst_clapper_gl_sink_get_widget (GstClapperGLSink * clapper_sink)
|
||||
{
|
||||
if (clapper_sink->widget != NULL)
|
||||
return clapper_sink->widget;
|
||||
|
||||
/* Ensure GTK is initialized, this has no side effect if it was already
|
||||
* initialized. Also, we do that lazily, so the application can be first */
|
||||
if (!gtk_init_check ()) {
|
||||
GST_ERROR_OBJECT (clapper_sink, "Could not ensure GTK initialization.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
g_assert (GST_CLAPPER_GL_SINK_GET_CLASS (clapper_sink)->create_widget);
|
||||
clapper_sink->widget = (GtkClapperGLWidget *)
|
||||
GST_CLAPPER_GL_SINK_GET_CLASS (clapper_sink)->create_widget ();
|
||||
|
||||
clapper_sink->bind_aspect_ratio =
|
||||
g_object_bind_property (clapper_sink, "force-aspect-ratio", clapper_sink->widget,
|
||||
"force-aspect-ratio", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
|
||||
clapper_sink->bind_pixel_aspect_ratio =
|
||||
g_object_bind_property (clapper_sink, "pixel-aspect-ratio", clapper_sink->widget,
|
||||
"pixel-aspect-ratio", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
|
||||
clapper_sink->bind_ignore_textures =
|
||||
g_object_bind_property (clapper_sink, "ignore-textures", clapper_sink->widget,
|
||||
"ignore-textures", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
|
||||
|
||||
/* Take the floating ref, other wise the destruction of the container will
|
||||
* make this widget disappear possibly before we are done. */
|
||||
gst_object_ref_sink (clapper_sink->widget);
|
||||
|
||||
clapper_sink->widget_destroy_id = g_signal_connect (clapper_sink->widget, "destroy",
|
||||
G_CALLBACK (widget_destroy_cb), clapper_sink);
|
||||
|
||||
/* back pointer */
|
||||
gtk_clapper_gl_widget_set_element (GTK_CLAPPER_GL_WIDGET (clapper_sink->widget),
|
||||
GST_ELEMENT (clapper_sink));
|
||||
|
||||
return clapper_sink->widget;
|
||||
}
|
||||
|
||||
static void
|
||||
gst_clapper_gl_sink_get_property (GObject * object, guint prop_id,
|
||||
GValue * value, GParamSpec * pspec)
|
||||
{
|
||||
GstClapperGLSink *clapper_sink = GST_CLAPPER_GL_SINK (object);
|
||||
|
||||
switch (prop_id) {
|
||||
case PROP_WIDGET:
|
||||
{
|
||||
GObject *widget = NULL;
|
||||
|
||||
GST_OBJECT_LOCK (clapper_sink);
|
||||
if (clapper_sink->widget != NULL)
|
||||
widget = G_OBJECT (clapper_sink->widget);
|
||||
GST_OBJECT_UNLOCK (clapper_sink);
|
||||
|
||||
if (!widget)
|
||||
widget =
|
||||
gst_gtk_invoke_on_main ((GThreadFunc) gst_clapper_gl_sink_get_widget,
|
||||
clapper_sink);
|
||||
|
||||
g_value_set_object (value, widget);
|
||||
break;
|
||||
}
|
||||
case PROP_FORCE_ASPECT_RATIO:
|
||||
g_value_set_boolean (value, clapper_sink->force_aspect_ratio);
|
||||
break;
|
||||
case PROP_PIXEL_ASPECT_RATIO:
|
||||
gst_value_set_fraction (value, clapper_sink->par_n, clapper_sink->par_d);
|
||||
break;
|
||||
case PROP_IGNORE_TEXTURES:
|
||||
g_value_set_boolean (value, clapper_sink->ignore_textures);
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
gst_clapper_gl_sink_set_property (GObject * object, guint prop_id,
|
||||
const GValue * value, GParamSpec * pspec)
|
||||
{
|
||||
GstClapperGLSink *clapper_sink = GST_CLAPPER_GL_SINK (object);
|
||||
|
||||
switch (prop_id) {
|
||||
case PROP_FORCE_ASPECT_RATIO:
|
||||
clapper_sink->force_aspect_ratio = g_value_get_boolean (value);
|
||||
break;
|
||||
case PROP_PIXEL_ASPECT_RATIO:
|
||||
clapper_sink->par_n = gst_value_get_fraction_numerator (value);
|
||||
clapper_sink->par_d = gst_value_get_fraction_denominator (value);
|
||||
break;
|
||||
case PROP_IGNORE_TEXTURES:
|
||||
clapper_sink->ignore_textures = g_value_get_boolean (value);
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
gst_clapper_gl_sink_navigation_send_event (GstNavigation * navigation,
|
||||
GstStructure * structure)
|
||||
{
|
||||
GstClapperGLSink *sink = GST_CLAPPER_GL_SINK (navigation);
|
||||
GstEvent *event;
|
||||
GstPad *pad;
|
||||
|
||||
event = gst_event_new_navigation (structure);
|
||||
pad = gst_pad_get_peer (GST_VIDEO_SINK_PAD (sink));
|
||||
|
||||
GST_TRACE_OBJECT (sink, "navigation event %" GST_PTR_FORMAT, structure);
|
||||
|
||||
if (GST_IS_PAD (pad) && GST_IS_EVENT (event)) {
|
||||
if (!gst_pad_send_event (pad, gst_event_ref (event))) {
|
||||
/* If upstream didn't handle the event we'll post a message with it
|
||||
* for the application in case it wants to do something with it */
|
||||
gst_element_post_message (GST_ELEMENT_CAST (sink),
|
||||
gst_navigation_message_new_event (GST_OBJECT_CAST (sink), event));
|
||||
}
|
||||
gst_event_unref (event);
|
||||
gst_object_unref (pad);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
gst_clapper_gl_sink_navigation_interface_init (GstNavigationInterface * iface)
|
||||
{
|
||||
iface->send_event = gst_clapper_gl_sink_navigation_send_event;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gst_clapper_gl_sink_propose_allocation (GstBaseSink * bsink, GstQuery * query)
|
||||
{
|
||||
GstClapperGLSink *clapper_sink = GST_CLAPPER_GL_SINK (bsink);
|
||||
GstBufferPool *pool = NULL;
|
||||
GstStructure *config;
|
||||
GstCaps *caps;
|
||||
GstVideoInfo info;
|
||||
guint size;
|
||||
gboolean need_pool;
|
||||
GstStructure *allocation_meta = NULL;
|
||||
gint display_width, display_height;
|
||||
|
||||
if (!clapper_sink->display || !clapper_sink->context)
|
||||
return FALSE;
|
||||
|
||||
gst_query_parse_allocation (query, &caps, &need_pool);
|
||||
|
||||
if (caps == NULL)
|
||||
goto no_caps;
|
||||
|
||||
if (!gst_video_info_from_caps (&info, caps))
|
||||
goto invalid_caps;
|
||||
|
||||
/* the normal size of a frame */
|
||||
size = info.size;
|
||||
|
||||
if (need_pool) {
|
||||
GST_DEBUG_OBJECT (clapper_sink, "create new pool");
|
||||
pool = gst_gl_buffer_pool_new (clapper_sink->context);
|
||||
|
||||
config = gst_buffer_pool_get_config (pool);
|
||||
gst_buffer_pool_config_set_params (config, caps, size, 0, 0);
|
||||
gst_buffer_pool_config_add_option (config,
|
||||
GST_BUFFER_POOL_OPTION_GL_SYNC_META);
|
||||
|
||||
if (!gst_buffer_pool_set_config (pool, config))
|
||||
goto config_failed;
|
||||
}
|
||||
|
||||
/* we need at least 2 buffer because we hold on to the last one */
|
||||
gst_query_add_allocation_pool (query, pool, size, 2, 0);
|
||||
if (pool)
|
||||
gst_object_unref (pool);
|
||||
|
||||
GST_OBJECT_LOCK (clapper_sink);
|
||||
display_width = clapper_sink->display_width;
|
||||
display_height = clapper_sink->display_height;
|
||||
GST_OBJECT_UNLOCK (clapper_sink);
|
||||
|
||||
if (display_width != 0 && display_height != 0) {
|
||||
GST_DEBUG_OBJECT (clapper_sink, "sending alloc query with size %dx%d",
|
||||
display_width, display_height);
|
||||
allocation_meta = gst_structure_new ("GstVideoOverlayCompositionMeta",
|
||||
"width", G_TYPE_UINT, display_width,
|
||||
"height", G_TYPE_UINT, display_height, NULL);
|
||||
}
|
||||
|
||||
gst_query_add_allocation_meta (query,
|
||||
GST_VIDEO_OVERLAY_COMPOSITION_META_API_TYPE, allocation_meta);
|
||||
|
||||
if (allocation_meta)
|
||||
gst_structure_free (allocation_meta);
|
||||
|
||||
/* we also support various metadata */
|
||||
gst_query_add_allocation_meta (query, GST_VIDEO_META_API_TYPE, 0);
|
||||
|
||||
if (clapper_sink->context->gl_vtable->FenceSync)
|
||||
gst_query_add_allocation_meta (query, GST_GL_SYNC_META_API_TYPE, 0);
|
||||
|
||||
return TRUE;
|
||||
|
||||
/* ERRORS */
|
||||
no_caps:
|
||||
{
|
||||
GST_DEBUG_OBJECT (bsink, "no caps specified");
|
||||
return FALSE;
|
||||
}
|
||||
invalid_caps:
|
||||
{
|
||||
GST_DEBUG_OBJECT (bsink, "invalid caps specified");
|
||||
return FALSE;
|
||||
}
|
||||
config_failed:
|
||||
{
|
||||
GST_DEBUG_OBJECT (bsink, "failed setting config");
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gst_clapper_gl_sink_query (GstBaseSink * bsink, GstQuery * query)
|
||||
{
|
||||
GstClapperGLSink *clapper_sink = GST_CLAPPER_GL_SINK (bsink);
|
||||
gboolean res = FALSE;
|
||||
|
||||
switch (GST_QUERY_TYPE (query)) {
|
||||
case GST_QUERY_CONTEXT:
|
||||
{
|
||||
if (gst_gl_handle_context_query ((GstElement *) clapper_sink, query,
|
||||
clapper_sink->display, clapper_sink->context, clapper_sink->gtk_context))
|
||||
return TRUE;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
res = GST_BASE_SINK_CLASS (parent_class)->query (bsink, query);
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gst_clapper_gl_sink_start_on_main (GstBaseSink * bsink)
|
||||
{
|
||||
GstClapperGLSink *gst_sink = GST_CLAPPER_GL_SINK (bsink);
|
||||
GstClapperGLSinkClass *klass = GST_CLAPPER_GL_SINK_GET_CLASS (bsink);
|
||||
GtkWidget *toplevel;
|
||||
GtkRoot *root;
|
||||
|
||||
if (gst_clapper_gl_sink_get_widget (gst_sink) == NULL)
|
||||
return FALSE;
|
||||
|
||||
/* After this point, clapper_sink->widget will always be set */
|
||||
|
||||
root = gtk_widget_get_root (GTK_WIDGET (gst_sink->widget));
|
||||
if (!GTK_IS_ROOT (root)) {
|
||||
GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (gst_sink->widget));
|
||||
if (parent) {
|
||||
GtkWidget *temp_parent;
|
||||
while ((temp_parent = gtk_widget_get_parent (parent)))
|
||||
parent = temp_parent;
|
||||
}
|
||||
toplevel = (parent) ? parent : GTK_WIDGET (gst_sink->widget);
|
||||
|
||||
/* sanity check */
|
||||
g_assert (klass->window_title);
|
||||
|
||||
/* User did not add widget its own UI, let's popup a new GtkWindow to
|
||||
* make gst-launch-1.0 work. */
|
||||
gst_sink->window = gtk_window_new ();
|
||||
gtk_window_set_default_size (GTK_WINDOW (gst_sink->window), 640, 480);
|
||||
gtk_window_set_title (GTK_WINDOW (gst_sink->window), klass->window_title);
|
||||
gtk_window_set_child (GTK_WINDOW (gst_sink->window), toplevel);
|
||||
|
||||
gst_sink->window_destroy_id = g_signal_connect (
|
||||
GTK_WINDOW (gst_sink->window),
|
||||
"destroy", G_CALLBACK (window_destroy_cb), gst_sink);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gst_clapper_gl_sink_start (GstBaseSink * bsink)
|
||||
{
|
||||
GstClapperGLSink *clapper_sink = GST_CLAPPER_GL_SINK (bsink);
|
||||
GtkClapperGLWidget *clapper_widget;
|
||||
|
||||
if (!(! !gst_gtk_invoke_on_main ((GThreadFunc) (GCallback)
|
||||
gst_clapper_gl_sink_start_on_main, bsink)))
|
||||
return FALSE;
|
||||
|
||||
/* After this point, clapper_sink->widget will always be set */
|
||||
clapper_widget = GTK_CLAPPER_GL_WIDGET (clapper_sink->widget);
|
||||
|
||||
if (!gtk_clapper_gl_widget_init_winsys (clapper_widget)) {
|
||||
GST_ELEMENT_ERROR (bsink, RESOURCE, NOT_FOUND, ("%s",
|
||||
"Failed to initialize OpenGL with GTK"), (NULL));
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!clapper_sink->display)
|
||||
clapper_sink->display = gtk_clapper_gl_widget_get_display (clapper_widget);
|
||||
if (!clapper_sink->context)
|
||||
clapper_sink->context = gtk_clapper_gl_widget_get_context (clapper_widget);
|
||||
if (!clapper_sink->gtk_context)
|
||||
clapper_sink->gtk_context = gtk_clapper_gl_widget_get_gtk_context (clapper_widget);
|
||||
|
||||
if (!clapper_sink->display || !clapper_sink->context || !clapper_sink->gtk_context) {
|
||||
GST_ELEMENT_ERROR (bsink, RESOURCE, NOT_FOUND, ("%s",
|
||||
"Failed to retrieve OpenGL context from GTK"), (NULL));
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
gst_gl_element_propagate_display_context (GST_ELEMENT (bsink),
|
||||
clapper_sink->display);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gst_clapper_gl_sink_stop_on_main (GstBaseSink * bsink)
|
||||
{
|
||||
GstClapperGLSink *gst_sink = GST_CLAPPER_GL_SINK (bsink);
|
||||
|
||||
if (gst_sink->window) {
|
||||
gtk_window_destroy (GTK_WINDOW (gst_sink->window));
|
||||
gst_sink->window = NULL;
|
||||
gst_sink->widget = NULL;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gst_clapper_gl_sink_stop (GstBaseSink * bsink)
|
||||
{
|
||||
GstClapperGLSink *clapper_sink = GST_CLAPPER_GL_SINK (bsink);
|
||||
|
||||
if (clapper_sink->display) {
|
||||
gst_object_unref (clapper_sink->display);
|
||||
clapper_sink->display = NULL;
|
||||
}
|
||||
if (clapper_sink->context) {
|
||||
gst_object_unref (clapper_sink->context);
|
||||
clapper_sink->context = NULL;
|
||||
}
|
||||
if (clapper_sink->gtk_context) {
|
||||
gst_object_unref (clapper_sink->gtk_context);
|
||||
clapper_sink->gtk_context = NULL;
|
||||
}
|
||||
if (clapper_sink->window)
|
||||
return ! !gst_gtk_invoke_on_main ((GThreadFunc) (GCallback)
|
||||
gst_clapper_gl_sink_stop_on_main, bsink);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
gst_gtk_window_show_all_and_unref (GtkWidget * window)
|
||||
{
|
||||
gtk_window_present (GTK_WINDOW (window));
|
||||
g_object_unref (window);
|
||||
}
|
||||
|
||||
static GstStateChangeReturn
|
||||
gst_clapper_gl_sink_change_state (GstElement * element, GstStateChange transition)
|
||||
{
|
||||
GstClapperGLSink *clapper_sink = GST_CLAPPER_GL_SINK (element);
|
||||
GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS;
|
||||
|
||||
GST_DEBUG_OBJECT (element, "changing state: %s => %s",
|
||||
gst_element_state_get_name (GST_STATE_TRANSITION_CURRENT (transition)),
|
||||
gst_element_state_get_name (GST_STATE_TRANSITION_NEXT (transition)));
|
||||
|
||||
ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
|
||||
if (ret == GST_STATE_CHANGE_FAILURE)
|
||||
return ret;
|
||||
|
||||
switch (transition) {
|
||||
case GST_STATE_CHANGE_READY_TO_PAUSED:
|
||||
{
|
||||
GtkWindow *window = NULL;
|
||||
|
||||
GST_OBJECT_LOCK (clapper_sink);
|
||||
if (clapper_sink->window)
|
||||
window = g_object_ref (GTK_WINDOW (clapper_sink->window));
|
||||
GST_OBJECT_UNLOCK (clapper_sink);
|
||||
|
||||
if (window) {
|
||||
gst_gtk_invoke_on_main ((GThreadFunc) (GCallback)
|
||||
gst_gtk_window_show_all_and_unref, window);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case GST_STATE_CHANGE_PAUSED_TO_READY:
|
||||
GST_OBJECT_LOCK (clapper_sink);
|
||||
if (clapper_sink->widget)
|
||||
gtk_clapper_gl_widget_set_buffer (clapper_sink->widget, NULL);
|
||||
GST_OBJECT_UNLOCK (clapper_sink);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void
|
||||
gst_clapper_gl_sink_get_times (GstBaseSink * bsink, GstBuffer * buf,
|
||||
GstClockTime * start, GstClockTime * end)
|
||||
{
|
||||
GstClapperGLSink *clapper_sink = GST_CLAPPER_GL_SINK (bsink);
|
||||
|
||||
if (GST_BUFFER_TIMESTAMP_IS_VALID (buf)) {
|
||||
*start = GST_BUFFER_TIMESTAMP (buf);
|
||||
if (GST_BUFFER_DURATION_IS_VALID (buf))
|
||||
*end = *start + GST_BUFFER_DURATION (buf);
|
||||
else {
|
||||
if (GST_VIDEO_INFO_FPS_N (&clapper_sink->v_info) > 0) {
|
||||
*end = *start +
|
||||
gst_util_uint64_scale_int (GST_SECOND,
|
||||
GST_VIDEO_INFO_FPS_D (&clapper_sink->v_info),
|
||||
GST_VIDEO_INFO_FPS_N (&clapper_sink->v_info));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static GstCaps *
|
||||
gst_clapper_gl_sink_get_caps (GstBaseSink * bsink, GstCaps * filter)
|
||||
{
|
||||
GstCaps *tmp = NULL;
|
||||
GstCaps *result = NULL;
|
||||
|
||||
tmp = gst_pad_get_pad_template_caps (GST_BASE_SINK_PAD (bsink));
|
||||
|
||||
if (filter) {
|
||||
GST_DEBUG_OBJECT (bsink, "intersecting with filter caps %" GST_PTR_FORMAT,
|
||||
filter);
|
||||
|
||||
result = gst_caps_intersect_full (filter, tmp, GST_CAPS_INTERSECT_FIRST);
|
||||
gst_caps_unref (tmp);
|
||||
} else {
|
||||
result = tmp;
|
||||
}
|
||||
|
||||
result = gst_gl_overlay_compositor_add_caps (result);
|
||||
|
||||
GST_DEBUG_OBJECT (bsink, "returning caps: %" GST_PTR_FORMAT, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gst_clapper_gl_sink_set_caps (GstBaseSink * bsink, GstCaps * caps)
|
||||
{
|
||||
GstClapperGLSink *clapper_sink = GST_CLAPPER_GL_SINK (bsink);
|
||||
|
||||
GST_DEBUG ("set caps with %" GST_PTR_FORMAT, caps);
|
||||
|
||||
if (!gst_video_info_from_caps (&clapper_sink->v_info, caps))
|
||||
return FALSE;
|
||||
|
||||
GST_OBJECT_LOCK (clapper_sink);
|
||||
|
||||
if (clapper_sink->widget == NULL) {
|
||||
GST_OBJECT_UNLOCK (clapper_sink);
|
||||
GST_ELEMENT_ERROR (clapper_sink, RESOURCE, NOT_FOUND,
|
||||
("%s", "Output widget was destroyed"), (NULL));
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!gtk_clapper_gl_widget_set_format (clapper_sink->widget, &clapper_sink->v_info)) {
|
||||
GST_OBJECT_UNLOCK (clapper_sink);
|
||||
return FALSE;
|
||||
}
|
||||
GST_OBJECT_UNLOCK (clapper_sink);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static GstFlowReturn
|
||||
gst_clapper_gl_sink_show_frame (GstVideoSink * vsink, GstBuffer * buf)
|
||||
{
|
||||
GstClapperGLSink *clapper_sink;
|
||||
|
||||
GST_TRACE ("rendering buffer:%p", buf);
|
||||
|
||||
clapper_sink = GST_CLAPPER_GL_SINK (vsink);
|
||||
|
||||
GST_OBJECT_LOCK (clapper_sink);
|
||||
|
||||
if (clapper_sink->widget == NULL) {
|
||||
GST_OBJECT_UNLOCK (clapper_sink);
|
||||
GST_ELEMENT_ERROR (clapper_sink, RESOURCE, NOT_FOUND,
|
||||
("%s", "Output widget was destroyed"), (NULL));
|
||||
return GST_FLOW_ERROR;
|
||||
}
|
||||
|
||||
gtk_clapper_gl_widget_set_buffer (clapper_sink->widget, buf);
|
||||
|
||||
GST_OBJECT_UNLOCK (clapper_sink);
|
||||
|
||||
return GST_FLOW_OK;
|
||||
}
|
106
lib/gst/clapper/gtk4/gstclapperglsink.h
Normal file
106
lib/gst/clapper/gtk4/gstclapperglsink.h
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* GStreamer
|
||||
* Copyright (C) 2015 Matthew Waters <matthew@centricular.com>
|
||||
* Copyright (C) 2020 Rafał Dzięgiel <rafostar.github@gmail.com>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 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
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __GST_CLAPPER_GL_SINK_H__
|
||||
#define __GST_CLAPPER_GL_SINK_H__
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
#include <gst/gst.h>
|
||||
#include <gst/video/gstvideosink.h>
|
||||
#include <gst/video/video.h>
|
||||
#include <gst/gl/gl.h>
|
||||
|
||||
#include "gtkclapperglwidget.h"
|
||||
|
||||
#define GST_TYPE_CLAPPER_GL_SINK (gst_clapper_gl_sink_get_type())
|
||||
#define GST_CLAPPER_GL_SINK(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_CLAPPER_GL_SINK,GstClapperGLSink))
|
||||
#define GST_CLAPPER_GL_SINK_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_CLAPPER_GL_SINK,GstClapperGLClass))
|
||||
#define GST_CLAPPER_GL_SINK_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_CLAPPER_GL_SINK, GstClapperGLSinkClass))
|
||||
#define GST_IS_CLAPPER_GL_SINK(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_CLAPPER_GL_SINK))
|
||||
#define GST_IS_CLAPPER_GL_SINK_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_CLAPPER_GL_SINK))
|
||||
#define GST_CLAPPER_GL_SINK_CAST(obj) ((GstClapperGLSink*)(obj))
|
||||
|
||||
G_BEGIN_DECLS
|
||||
typedef struct _GstClapperGLSink GstClapperGLSink;
|
||||
typedef struct _GstClapperGLSinkClass GstClapperGLSinkClass;
|
||||
|
||||
GType gst_clapper_gl_sink_get_type (void);
|
||||
|
||||
/**
|
||||
* GstClapperGLSink:
|
||||
*
|
||||
* Opaque #GstClapperGLSink object
|
||||
*/
|
||||
struct _GstClapperGLSink
|
||||
{
|
||||
/* <private> */
|
||||
GstVideoSink parent;
|
||||
|
||||
GstVideoInfo v_info;
|
||||
|
||||
GtkClapperGLWidget *widget;
|
||||
|
||||
/* properties */
|
||||
gboolean force_aspect_ratio;
|
||||
GBinding *bind_aspect_ratio;
|
||||
|
||||
gint par_n, par_d;
|
||||
GBinding *bind_pixel_aspect_ratio;
|
||||
|
||||
gboolean ignore_textures;
|
||||
GBinding *bind_ignore_textures;
|
||||
|
||||
GtkWidget *window;
|
||||
gulong widget_destroy_id;
|
||||
gulong window_destroy_id;
|
||||
|
||||
GstGLDisplay *display;
|
||||
GstGLContext *context;
|
||||
GstGLContext *gtk_context;
|
||||
|
||||
GstGLUpload *upload;
|
||||
GstBuffer *uploaded_buffer;
|
||||
|
||||
/* read/write with object lock */
|
||||
gint display_width, display_height;
|
||||
};
|
||||
|
||||
/**
|
||||
* GstClapperGLSinkClass:
|
||||
*
|
||||
* The #GstClapperGLSinkClass struct only contains private data
|
||||
*/
|
||||
struct _GstClapperGLSinkClass
|
||||
{
|
||||
GstVideoSinkClass object_class;
|
||||
|
||||
/* metadata */
|
||||
const gchar *window_title;
|
||||
|
||||
/* virtuals */
|
||||
GtkWidget* (*create_widget) (void);
|
||||
};
|
||||
|
||||
G_DEFINE_AUTOPTR_CLEANUP_FUNC (GstClapperGLSink, gst_object_unref)
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif /* __GST_CLAPPER_GL_SINK_H__ */
|
@@ -1,574 +0,0 @@
|
||||
/*
|
||||
* GStreamer
|
||||
* Copyright (C) 2015 Matthew Waters <matthew@centricular.com>
|
||||
* Copyright (C) 2020 Rafał Dzięgiel <rafostar.github@gmail.com>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 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
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
/**
|
||||
* SECTION:gtkgstsink
|
||||
* @title: GstGtkBaseSink
|
||||
*
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include "gstgtkbasesink.h"
|
||||
#include "gstgtkutils.h"
|
||||
|
||||
GST_DEBUG_CATEGORY (gst_debug_gtk_base_sink);
|
||||
#define GST_CAT_DEFAULT gst_debug_gtk_base_sink
|
||||
|
||||
#define DEFAULT_FORCE_ASPECT_RATIO TRUE
|
||||
#define DEFAULT_PAR_N 0
|
||||
#define DEFAULT_PAR_D 1
|
||||
#define DEFAULT_IGNORE_ALPHA TRUE
|
||||
#define DEFAULT_IGNORE_TEXTURES FALSE
|
||||
|
||||
static void gst_gtk_base_sink_finalize (GObject * object);
|
||||
static void gst_gtk_base_sink_set_property (GObject * object, guint prop_id,
|
||||
const GValue * value, GParamSpec * param_spec);
|
||||
static void gst_gtk_base_sink_get_property (GObject * object, guint prop_id,
|
||||
GValue * value, GParamSpec * param_spec);
|
||||
|
||||
static gboolean gst_gtk_base_sink_start (GstBaseSink * bsink);
|
||||
static gboolean gst_gtk_base_sink_stop (GstBaseSink * bsink);
|
||||
|
||||
static GstStateChangeReturn
|
||||
gst_gtk_base_sink_change_state (GstElement * element,
|
||||
GstStateChange transition);
|
||||
|
||||
static void gst_gtk_base_sink_get_times (GstBaseSink * bsink, GstBuffer * buf,
|
||||
GstClockTime * start, GstClockTime * end);
|
||||
static gboolean gst_gtk_base_sink_set_caps (GstBaseSink * bsink,
|
||||
GstCaps * caps);
|
||||
static GstFlowReturn gst_gtk_base_sink_show_frame (GstVideoSink * bsink,
|
||||
GstBuffer * buf);
|
||||
|
||||
static void
|
||||
gst_gtk_base_sink_navigation_interface_init (GstNavigationInterface * iface);
|
||||
|
||||
enum
|
||||
{
|
||||
PROP_0,
|
||||
PROP_WIDGET,
|
||||
PROP_FORCE_ASPECT_RATIO,
|
||||
PROP_PIXEL_ASPECT_RATIO,
|
||||
PROP_IGNORE_ALPHA,
|
||||
PROP_IGNORE_TEXTURES,
|
||||
};
|
||||
|
||||
#define gst_gtk_base_sink_parent_class parent_class
|
||||
G_DEFINE_ABSTRACT_TYPE_WITH_CODE (GstGtkBaseSink, gst_gtk_base_sink,
|
||||
GST_TYPE_VIDEO_SINK,
|
||||
G_IMPLEMENT_INTERFACE (GST_TYPE_NAVIGATION,
|
||||
gst_gtk_base_sink_navigation_interface_init);
|
||||
GST_DEBUG_CATEGORY_INIT (gst_debug_gtk_base_sink,
|
||||
"gtkbasesink", 0, "GTK Video Sink base class"));
|
||||
|
||||
|
||||
static void
|
||||
gst_gtk_base_sink_class_init (GstGtkBaseSinkClass * klass)
|
||||
{
|
||||
GObjectClass *gobject_class;
|
||||
GstElementClass *gstelement_class;
|
||||
GstBaseSinkClass *gstbasesink_class;
|
||||
GstVideoSinkClass *gstvideosink_class;
|
||||
|
||||
gobject_class = (GObjectClass *) klass;
|
||||
gstelement_class = (GstElementClass *) klass;
|
||||
gstbasesink_class = (GstBaseSinkClass *) klass;
|
||||
gstvideosink_class = (GstVideoSinkClass *) klass;
|
||||
|
||||
gobject_class->set_property = gst_gtk_base_sink_set_property;
|
||||
gobject_class->get_property = gst_gtk_base_sink_get_property;
|
||||
|
||||
g_object_class_install_property (gobject_class, PROP_WIDGET,
|
||||
g_param_spec_object ("widget", "GTK Widget",
|
||||
"The GtkWidget to place in the widget hierarchy "
|
||||
"(must only be get from the GTK main thread)",
|
||||
GTK_TYPE_WIDGET, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
|
||||
|
||||
g_object_class_install_property (gobject_class, PROP_FORCE_ASPECT_RATIO,
|
||||
g_param_spec_boolean ("force-aspect-ratio",
|
||||
"Force aspect ratio",
|
||||
"When enabled, scaling will respect original aspect ratio",
|
||||
DEFAULT_FORCE_ASPECT_RATIO,
|
||||
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
||||
|
||||
g_object_class_install_property (gobject_class, PROP_PIXEL_ASPECT_RATIO,
|
||||
gst_param_spec_fraction ("pixel-aspect-ratio", "Pixel Aspect Ratio",
|
||||
"The pixel aspect ratio of the device", DEFAULT_PAR_N, DEFAULT_PAR_D,
|
||||
G_MAXINT, 1, 1, 1, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
||||
|
||||
/* Disabling alpha was removed in GTK4 */
|
||||
#if !defined(BUILD_FOR_GTK4)
|
||||
g_object_class_install_property (gobject_class, PROP_IGNORE_ALPHA,
|
||||
g_param_spec_boolean ("ignore-alpha", "Ignore Alpha",
|
||||
"When enabled, alpha will be ignored and converted to black",
|
||||
DEFAULT_IGNORE_ALPHA, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
||||
#endif
|
||||
|
||||
g_object_class_install_property (gobject_class, PROP_IGNORE_TEXTURES,
|
||||
g_param_spec_boolean ("ignore-textures", "Ignore Textures",
|
||||
"When enabled, textures will be ignored and not drawn",
|
||||
DEFAULT_IGNORE_TEXTURES, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
||||
|
||||
gobject_class->finalize = gst_gtk_base_sink_finalize;
|
||||
|
||||
gstelement_class->change_state = gst_gtk_base_sink_change_state;
|
||||
gstbasesink_class->set_caps = gst_gtk_base_sink_set_caps;
|
||||
gstbasesink_class->get_times = gst_gtk_base_sink_get_times;
|
||||
gstbasesink_class->start = gst_gtk_base_sink_start;
|
||||
gstbasesink_class->stop = gst_gtk_base_sink_stop;
|
||||
|
||||
gstvideosink_class->show_frame = gst_gtk_base_sink_show_frame;
|
||||
|
||||
gst_type_mark_as_plugin_api (GST_TYPE_GTK_BASE_SINK, 0);
|
||||
}
|
||||
|
||||
static void
|
||||
gst_gtk_base_sink_init (GstGtkBaseSink * gtk_sink)
|
||||
{
|
||||
gtk_sink->force_aspect_ratio = DEFAULT_FORCE_ASPECT_RATIO;
|
||||
gtk_sink->par_n = DEFAULT_PAR_N;
|
||||
gtk_sink->par_d = DEFAULT_PAR_D;
|
||||
gtk_sink->ignore_alpha = DEFAULT_IGNORE_ALPHA;
|
||||
gtk_sink->ignore_textures = DEFAULT_IGNORE_TEXTURES;
|
||||
}
|
||||
|
||||
static void
|
||||
gst_gtk_base_sink_finalize (GObject * object)
|
||||
{
|
||||
GstGtkBaseSink *gtk_sink = GST_GTK_BASE_SINK (object);
|
||||
|
||||
GST_DEBUG ("finalizing base sink");
|
||||
|
||||
GST_OBJECT_LOCK (gtk_sink);
|
||||
if (gtk_sink->window && gtk_sink->window_destroy_id)
|
||||
g_signal_handler_disconnect (gtk_sink->window, gtk_sink->window_destroy_id);
|
||||
if (gtk_sink->widget && gtk_sink->widget_destroy_id)
|
||||
g_signal_handler_disconnect (gtk_sink->widget, gtk_sink->widget_destroy_id);
|
||||
|
||||
g_clear_object (>k_sink->widget);
|
||||
GST_OBJECT_UNLOCK (gtk_sink);
|
||||
|
||||
G_OBJECT_CLASS (parent_class)->finalize (object);
|
||||
}
|
||||
|
||||
static void
|
||||
widget_destroy_cb (GtkWidget * widget, GstGtkBaseSink * gtk_sink)
|
||||
{
|
||||
GST_OBJECT_LOCK (gtk_sink);
|
||||
g_clear_object (>k_sink->widget);
|
||||
GST_OBJECT_UNLOCK (gtk_sink);
|
||||
}
|
||||
|
||||
static void
|
||||
window_destroy_cb (GtkWidget * widget, GstGtkBaseSink * gtk_sink)
|
||||
{
|
||||
GST_OBJECT_LOCK (gtk_sink);
|
||||
if (gtk_sink->widget) {
|
||||
if (gtk_sink->widget_destroy_id) {
|
||||
g_signal_handler_disconnect (gtk_sink->widget,
|
||||
gtk_sink->widget_destroy_id);
|
||||
gtk_sink->widget_destroy_id = 0;
|
||||
}
|
||||
g_clear_object (>k_sink->widget);
|
||||
}
|
||||
gtk_sink->window = NULL;
|
||||
GST_OBJECT_UNLOCK (gtk_sink);
|
||||
}
|
||||
|
||||
static GtkGstBaseWidget *
|
||||
gst_gtk_base_sink_get_widget (GstGtkBaseSink * gtk_sink)
|
||||
{
|
||||
if (gtk_sink->widget != NULL)
|
||||
return gtk_sink->widget;
|
||||
|
||||
/* Ensure GTK is initialized, this has no side effect if it was already
|
||||
* initialized. Also, we do that lazily, so the application can be first */
|
||||
if (!gtk_init_check (
|
||||
#if !defined(BUILD_FOR_GTK4)
|
||||
NULL, NULL
|
||||
#endif
|
||||
)) {
|
||||
GST_ERROR_OBJECT (gtk_sink, "Could not ensure GTK initialization.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
g_assert (GST_GTK_BASE_SINK_GET_CLASS (gtk_sink)->create_widget);
|
||||
gtk_sink->widget = (GtkGstBaseWidget *)
|
||||
GST_GTK_BASE_SINK_GET_CLASS (gtk_sink)->create_widget ();
|
||||
|
||||
gtk_sink->bind_aspect_ratio =
|
||||
g_object_bind_property (gtk_sink, "force-aspect-ratio", gtk_sink->widget,
|
||||
"force-aspect-ratio", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
|
||||
gtk_sink->bind_pixel_aspect_ratio =
|
||||
g_object_bind_property (gtk_sink, "pixel-aspect-ratio", gtk_sink->widget,
|
||||
"pixel-aspect-ratio", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
|
||||
#if !defined(BUILD_FOR_GTK4)
|
||||
gtk_sink->bind_ignore_alpha =
|
||||
g_object_bind_property (gtk_sink, "ignore-alpha", gtk_sink->widget,
|
||||
"ignore-alpha", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
|
||||
#endif
|
||||
|
||||
gtk_sink->bind_ignore_textures =
|
||||
g_object_bind_property (gtk_sink, "ignore-textures", gtk_sink->widget,
|
||||
"ignore-textures", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
|
||||
|
||||
/* Take the floating ref, other wise the destruction of the container will
|
||||
* make this widget disappear possibly before we are done. */
|
||||
gst_object_ref_sink (gtk_sink->widget);
|
||||
|
||||
gtk_sink->widget_destroy_id = g_signal_connect (gtk_sink->widget, "destroy",
|
||||
G_CALLBACK (widget_destroy_cb), gtk_sink);
|
||||
|
||||
/* back pointer */
|
||||
gtk_gst_base_widget_set_element (GTK_GST_BASE_WIDGET (gtk_sink->widget),
|
||||
GST_ELEMENT (gtk_sink));
|
||||
|
||||
return gtk_sink->widget;
|
||||
}
|
||||
|
||||
static void
|
||||
gst_gtk_base_sink_get_property (GObject * object, guint prop_id,
|
||||
GValue * value, GParamSpec * pspec)
|
||||
{
|
||||
GstGtkBaseSink *gtk_sink = GST_GTK_BASE_SINK (object);
|
||||
|
||||
switch (prop_id) {
|
||||
case PROP_WIDGET:
|
||||
{
|
||||
GObject *widget = NULL;
|
||||
|
||||
GST_OBJECT_LOCK (gtk_sink);
|
||||
if (gtk_sink->widget != NULL)
|
||||
widget = G_OBJECT (gtk_sink->widget);
|
||||
GST_OBJECT_UNLOCK (gtk_sink);
|
||||
|
||||
if (!widget)
|
||||
widget =
|
||||
gst_gtk_invoke_on_main ((GThreadFunc) gst_gtk_base_sink_get_widget,
|
||||
gtk_sink);
|
||||
|
||||
g_value_set_object (value, widget);
|
||||
break;
|
||||
}
|
||||
case PROP_FORCE_ASPECT_RATIO:
|
||||
g_value_set_boolean (value, gtk_sink->force_aspect_ratio);
|
||||
break;
|
||||
case PROP_PIXEL_ASPECT_RATIO:
|
||||
gst_value_set_fraction (value, gtk_sink->par_n, gtk_sink->par_d);
|
||||
break;
|
||||
case PROP_IGNORE_ALPHA:
|
||||
g_value_set_boolean (value, gtk_sink->ignore_alpha);
|
||||
break;
|
||||
case PROP_IGNORE_TEXTURES:
|
||||
g_value_set_boolean (value, gtk_sink->ignore_textures);
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
gst_gtk_base_sink_set_property (GObject * object, guint prop_id,
|
||||
const GValue * value, GParamSpec * pspec)
|
||||
{
|
||||
GstGtkBaseSink *gtk_sink = GST_GTK_BASE_SINK (object);
|
||||
|
||||
switch (prop_id) {
|
||||
case PROP_FORCE_ASPECT_RATIO:
|
||||
gtk_sink->force_aspect_ratio = g_value_get_boolean (value);
|
||||
break;
|
||||
case PROP_PIXEL_ASPECT_RATIO:
|
||||
gtk_sink->par_n = gst_value_get_fraction_numerator (value);
|
||||
gtk_sink->par_d = gst_value_get_fraction_denominator (value);
|
||||
break;
|
||||
case PROP_IGNORE_ALPHA:
|
||||
gtk_sink->ignore_alpha = g_value_get_boolean (value);
|
||||
break;
|
||||
case PROP_IGNORE_TEXTURES:
|
||||
gtk_sink->ignore_textures = g_value_get_boolean (value);
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
gst_gtk_base_sink_navigation_send_event (GstNavigation * navigation,
|
||||
GstStructure * structure)
|
||||
{
|
||||
GstGtkBaseSink *sink = GST_GTK_BASE_SINK (navigation);
|
||||
GstEvent *event;
|
||||
GstPad *pad;
|
||||
|
||||
event = gst_event_new_navigation (structure);
|
||||
pad = gst_pad_get_peer (GST_VIDEO_SINK_PAD (sink));
|
||||
|
||||
GST_TRACE_OBJECT (sink, "navigation event %" GST_PTR_FORMAT, structure);
|
||||
|
||||
if (GST_IS_PAD (pad) && GST_IS_EVENT (event)) {
|
||||
if (!gst_pad_send_event (pad, gst_event_ref (event))) {
|
||||
/* If upstream didn't handle the event we'll post a message with it
|
||||
* for the application in case it wants to do something with it */
|
||||
gst_element_post_message (GST_ELEMENT_CAST (sink),
|
||||
gst_navigation_message_new_event (GST_OBJECT_CAST (sink), event));
|
||||
}
|
||||
gst_event_unref (event);
|
||||
gst_object_unref (pad);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
gst_gtk_base_sink_navigation_interface_init (GstNavigationInterface * iface)
|
||||
{
|
||||
iface->send_event = gst_gtk_base_sink_navigation_send_event;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gst_gtk_base_sink_start_on_main (GstBaseSink * bsink)
|
||||
{
|
||||
GstGtkBaseSink *gst_sink = GST_GTK_BASE_SINK (bsink);
|
||||
GstGtkBaseSinkClass *klass = GST_GTK_BASE_SINK_GET_CLASS (bsink);
|
||||
GtkWidget *toplevel;
|
||||
#if defined(BUILD_FOR_GTK4)
|
||||
GtkRoot *root;
|
||||
#endif
|
||||
|
||||
if (gst_gtk_base_sink_get_widget (gst_sink) == NULL)
|
||||
return FALSE;
|
||||
|
||||
/* After this point, gtk_sink->widget will always be set */
|
||||
|
||||
#if defined(BUILD_FOR_GTK4)
|
||||
root = gtk_widget_get_root (GTK_WIDGET (gst_sink->widget));
|
||||
if (!GTK_IS_ROOT (root)) {
|
||||
GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (gst_sink->widget));
|
||||
if (parent) {
|
||||
GtkWidget *temp_parent;
|
||||
while ((temp_parent = gtk_widget_get_parent (parent)))
|
||||
parent = temp_parent;
|
||||
}
|
||||
toplevel = (parent) ? parent : GTK_WIDGET (gst_sink->widget);
|
||||
#else
|
||||
toplevel = gtk_widget_get_toplevel (GTK_WIDGET (gst_sink->widget));
|
||||
if (!gtk_widget_is_toplevel (toplevel)) {
|
||||
#endif
|
||||
/* sanity check */
|
||||
g_assert (klass->window_title);
|
||||
|
||||
/* User did not add widget its own UI, let's popup a new GtkWindow to
|
||||
* make gst-launch-1.0 work. */
|
||||
gst_sink->window = gtk_window_new (
|
||||
#if !defined(BUILD_FOR_GTK4)
|
||||
GTK_WINDOW_TOPLEVEL
|
||||
#endif
|
||||
);
|
||||
gtk_window_set_default_size (GTK_WINDOW (gst_sink->window), 640, 480);
|
||||
gtk_window_set_title (GTK_WINDOW (gst_sink->window), klass->window_title);
|
||||
#if defined(BUILD_FOR_GTK4)
|
||||
gtk_window_set_child (GTK_WINDOW (
|
||||
#else
|
||||
gtk_container_add (GTK_CONTAINER (
|
||||
#endif
|
||||
gst_sink->window), toplevel);
|
||||
|
||||
gst_sink->window_destroy_id = g_signal_connect (
|
||||
#if defined(BUILD_FOR_GTK4)
|
||||
GTK_WINDOW (gst_sink->window),
|
||||
#else
|
||||
gst_sink->window,
|
||||
#endif
|
||||
"destroy", G_CALLBACK (window_destroy_cb), gst_sink);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gst_gtk_base_sink_start (GstBaseSink * bsink)
|
||||
{
|
||||
return ! !gst_gtk_invoke_on_main ((GThreadFunc) (GCallback)
|
||||
gst_gtk_base_sink_start_on_main, bsink);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gst_gtk_base_sink_stop_on_main (GstBaseSink * bsink)
|
||||
{
|
||||
GstGtkBaseSink *gst_sink = GST_GTK_BASE_SINK (bsink);
|
||||
|
||||
if (gst_sink->window) {
|
||||
#if defined(BUILD_FOR_GTK4)
|
||||
gtk_window_destroy (GTK_WINDOW (gst_sink->window));
|
||||
#else
|
||||
gtk_widget_destroy (gst_sink->window);
|
||||
#endif
|
||||
gst_sink->window = NULL;
|
||||
gst_sink->widget = NULL;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gst_gtk_base_sink_stop (GstBaseSink * bsink)
|
||||
{
|
||||
GstGtkBaseSink *gst_sink = GST_GTK_BASE_SINK (bsink);
|
||||
|
||||
if (gst_sink->window)
|
||||
return ! !gst_gtk_invoke_on_main ((GThreadFunc) (GCallback)
|
||||
gst_gtk_base_sink_stop_on_main, bsink);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
gst_gtk_window_show_all_and_unref (GtkWidget * window)
|
||||
{
|
||||
#if defined(BUILD_FOR_GTK4)
|
||||
gtk_window_present (GTK_WINDOW (window));
|
||||
#else
|
||||
gtk_widget_show_all (window);
|
||||
#endif
|
||||
g_object_unref (window);
|
||||
}
|
||||
|
||||
static GstStateChangeReturn
|
||||
gst_gtk_base_sink_change_state (GstElement * element, GstStateChange transition)
|
||||
{
|
||||
GstGtkBaseSink *gtk_sink = GST_GTK_BASE_SINK (element);
|
||||
GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS;
|
||||
|
||||
GST_DEBUG_OBJECT (element, "changing state: %s => %s",
|
||||
gst_element_state_get_name (GST_STATE_TRANSITION_CURRENT (transition)),
|
||||
gst_element_state_get_name (GST_STATE_TRANSITION_NEXT (transition)));
|
||||
|
||||
ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, transition);
|
||||
if (ret == GST_STATE_CHANGE_FAILURE)
|
||||
return ret;
|
||||
|
||||
switch (transition) {
|
||||
case GST_STATE_CHANGE_READY_TO_PAUSED:
|
||||
{
|
||||
GtkWindow *window = NULL;
|
||||
|
||||
GST_OBJECT_LOCK (gtk_sink);
|
||||
if (gtk_sink->window)
|
||||
window = g_object_ref (GTK_WINDOW (gtk_sink->window));
|
||||
GST_OBJECT_UNLOCK (gtk_sink);
|
||||
|
||||
if (window) {
|
||||
gst_gtk_invoke_on_main ((GThreadFunc) (GCallback)
|
||||
gst_gtk_window_show_all_and_unref, window);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case GST_STATE_CHANGE_PAUSED_TO_READY:
|
||||
GST_OBJECT_LOCK (gtk_sink);
|
||||
if (gtk_sink->widget)
|
||||
gtk_gst_base_widget_set_buffer (gtk_sink->widget, NULL);
|
||||
GST_OBJECT_UNLOCK (gtk_sink);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void
|
||||
gst_gtk_base_sink_get_times (GstBaseSink * bsink, GstBuffer * buf,
|
||||
GstClockTime * start, GstClockTime * end)
|
||||
{
|
||||
GstGtkBaseSink *gtk_sink;
|
||||
|
||||
gtk_sink = GST_GTK_BASE_SINK (bsink);
|
||||
|
||||
if (GST_BUFFER_TIMESTAMP_IS_VALID (buf)) {
|
||||
*start = GST_BUFFER_TIMESTAMP (buf);
|
||||
if (GST_BUFFER_DURATION_IS_VALID (buf))
|
||||
*end = *start + GST_BUFFER_DURATION (buf);
|
||||
else {
|
||||
if (GST_VIDEO_INFO_FPS_N (>k_sink->v_info) > 0) {
|
||||
*end = *start +
|
||||
gst_util_uint64_scale_int (GST_SECOND,
|
||||
GST_VIDEO_INFO_FPS_D (>k_sink->v_info),
|
||||
GST_VIDEO_INFO_FPS_N (>k_sink->v_info));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gboolean
|
||||
gst_gtk_base_sink_set_caps (GstBaseSink * bsink, GstCaps * caps)
|
||||
{
|
||||
GstGtkBaseSink *gtk_sink = GST_GTK_BASE_SINK (bsink);
|
||||
|
||||
GST_DEBUG ("set caps with %" GST_PTR_FORMAT, caps);
|
||||
|
||||
if (!gst_video_info_from_caps (>k_sink->v_info, caps))
|
||||
return FALSE;
|
||||
|
||||
GST_OBJECT_LOCK (gtk_sink);
|
||||
|
||||
if (gtk_sink->widget == NULL) {
|
||||
GST_OBJECT_UNLOCK (gtk_sink);
|
||||
GST_ELEMENT_ERROR (gtk_sink, RESOURCE, NOT_FOUND,
|
||||
("%s", "Output widget was destroyed"), (NULL));
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!gtk_gst_base_widget_set_format (gtk_sink->widget, >k_sink->v_info)) {
|
||||
GST_OBJECT_UNLOCK (gtk_sink);
|
||||
return FALSE;
|
||||
}
|
||||
GST_OBJECT_UNLOCK (gtk_sink);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static GstFlowReturn
|
||||
gst_gtk_base_sink_show_frame (GstVideoSink * vsink, GstBuffer * buf)
|
||||
{
|
||||
GstGtkBaseSink *gtk_sink;
|
||||
|
||||
GST_TRACE ("rendering buffer:%p", buf);
|
||||
|
||||
gtk_sink = GST_GTK_BASE_SINK (vsink);
|
||||
|
||||
GST_OBJECT_LOCK (gtk_sink);
|
||||
|
||||
if (gtk_sink->widget == NULL) {
|
||||
GST_OBJECT_UNLOCK (gtk_sink);
|
||||
GST_ELEMENT_ERROR (gtk_sink, RESOURCE, NOT_FOUND,
|
||||
("%s", "Output widget was destroyed"), (NULL));
|
||||
return GST_FLOW_ERROR;
|
||||
}
|
||||
|
||||
gtk_gst_base_widget_set_buffer (gtk_sink->widget, buf);
|
||||
|
||||
GST_OBJECT_UNLOCK (gtk_sink);
|
||||
|
||||
return GST_FLOW_OK;
|
||||
}
|
@@ -1,99 +0,0 @@
|
||||
/*
|
||||
* GStreamer
|
||||
* Copyright (C) 2015 Matthew Waters <matthew@centricular.com>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 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
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __GST_GTK_BASE_SINK_H__
|
||||
#define __GST_GTK_BASE_SINK_H__
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
#include <gst/gst.h>
|
||||
#include <gst/video/gstvideosink.h>
|
||||
#include <gst/video/video.h>
|
||||
|
||||
#include "gtkgstbasewidget.h"
|
||||
|
||||
#define GST_TYPE_GTK_BASE_SINK (gst_gtk_base_sink_get_type())
|
||||
#define GST_GTK_BASE_SINK(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_GTK_BASE_SINK,GstGtkBaseSink))
|
||||
#define GST_GTK_BASE_SINK_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_GTK_BASE_SINK,GstGtkBaseSinkClass))
|
||||
#define GST_GTK_BASE_SINK_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_GTK_BASE_SINK, GstGtkBaseSinkClass))
|
||||
#define GST_IS_GTK_BASE_SINK(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_GTK_BASE_SINK))
|
||||
#define GST_IS_GTK_BASE_SINK_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_GTK_BASE_SINK))
|
||||
#define GST_GTK_BASE_SINK_CAST(obj) ((GstGtkBaseSink*)(obj))
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
typedef struct _GstGtkBaseSink GstGtkBaseSink;
|
||||
typedef struct _GstGtkBaseSinkClass GstGtkBaseSinkClass;
|
||||
|
||||
GType gst_gtk_base_sink_get_type (void);
|
||||
|
||||
/**
|
||||
* GstGtkBaseSink:
|
||||
*
|
||||
* Opaque #GstGtkBaseSink object
|
||||
*/
|
||||
struct _GstGtkBaseSink
|
||||
{
|
||||
/* <private> */
|
||||
GstVideoSink parent;
|
||||
|
||||
GstVideoInfo v_info;
|
||||
|
||||
GtkGstBaseWidget *widget;
|
||||
|
||||
/* properties */
|
||||
gboolean force_aspect_ratio;
|
||||
GBinding *bind_aspect_ratio;
|
||||
|
||||
gint par_n;
|
||||
gint par_d;
|
||||
GBinding *bind_pixel_aspect_ratio;
|
||||
|
||||
gboolean ignore_alpha;
|
||||
GBinding *bind_ignore_alpha;
|
||||
|
||||
gboolean ignore_textures;
|
||||
GBinding *bind_ignore_textures;
|
||||
|
||||
GtkWidget *window;
|
||||
gulong widget_destroy_id;
|
||||
gulong window_destroy_id;
|
||||
};
|
||||
|
||||
/**
|
||||
* GstGtkBaseSinkClass:
|
||||
*
|
||||
* The #GstGtkBaseSinkClass struct only contains private data
|
||||
*/
|
||||
struct _GstGtkBaseSinkClass
|
||||
{
|
||||
GstVideoSinkClass object_class;
|
||||
|
||||
/* metadata */
|
||||
const gchar *window_title;
|
||||
|
||||
/* virtuals */
|
||||
GtkWidget* (*create_widget) (void);
|
||||
};
|
||||
|
||||
G_DEFINE_AUTOPTR_CLEANUP_FUNC (GstGtkBaseSink, gst_object_unref)
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif /* __GST_GTK_BASE_SINK_H__ */
|
@@ -1,336 +0,0 @@
|
||||
/*
|
||||
* GStreamer
|
||||
* Copyright (C) 2015 Matthew Waters <matthew@centricular.com>
|
||||
* Copyright (C) 2020 Rafał Dzięgiel <rafostar.github@gmail.com>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 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
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
/**
|
||||
* SECTION:element-gtkglsink
|
||||
* @title: gtkglsink
|
||||
*/
|
||||
|
||||
/**
|
||||
* SECTION:element-gtk4glsink
|
||||
* @title: gtk4glsink
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include <gst/gl/gstglfuncs.h>
|
||||
|
||||
#include "gtkconfig.h"
|
||||
#include "gstgtkglsink.h"
|
||||
#include "gtkgstglwidget.h"
|
||||
|
||||
GST_DEBUG_CATEGORY (gst_debug_gtk_gl_sink);
|
||||
#define GST_CAT_DEFAULT gst_debug_gtk_gl_sink
|
||||
|
||||
static gboolean gst_gtk_gl_sink_start (GstBaseSink * bsink);
|
||||
static gboolean gst_gtk_gl_sink_stop (GstBaseSink * bsink);
|
||||
static gboolean gst_gtk_gl_sink_query (GstBaseSink * bsink, GstQuery * query);
|
||||
static gboolean gst_gtk_gl_sink_propose_allocation (GstBaseSink * bsink,
|
||||
GstQuery * query);
|
||||
static GstCaps *gst_gtk_gl_sink_get_caps (GstBaseSink * bsink,
|
||||
GstCaps * filter);
|
||||
|
||||
static void gst_gtk_gl_sink_finalize (GObject * object);
|
||||
|
||||
static GstStaticPadTemplate gst_gtk_gl_sink_template =
|
||||
GST_STATIC_PAD_TEMPLATE ("sink",
|
||||
GST_PAD_SINK,
|
||||
GST_PAD_ALWAYS,
|
||||
GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE_WITH_FEATURES
|
||||
(GST_CAPS_FEATURE_MEMORY_GL_MEMORY, "RGBA") "; "
|
||||
GST_VIDEO_CAPS_MAKE_WITH_FEATURES
|
||||
(GST_CAPS_FEATURE_MEMORY_GL_MEMORY ", "
|
||||
GST_CAPS_FEATURE_META_GST_VIDEO_OVERLAY_COMPOSITION, "RGBA")));
|
||||
|
||||
#define gst_gtk_gl_sink_parent_class parent_class
|
||||
G_DEFINE_TYPE_WITH_CODE (GstGtkGLSink, gst_gtk_gl_sink,
|
||||
GST_TYPE_GTK_BASE_SINK, GST_DEBUG_CATEGORY_INIT (gst_debug_gtk_gl_sink,
|
||||
GTKCONFIG_GLSINK, 0, GTKCONFIG_NAME " GL Video Sink"));
|
||||
|
||||
static void
|
||||
gst_gtk_gl_sink_class_init (GstGtkGLSinkClass * klass)
|
||||
{
|
||||
GObjectClass *gobject_class;
|
||||
GstElementClass *gstelement_class;
|
||||
GstBaseSinkClass *gstbasesink_class;
|
||||
GstGtkBaseSinkClass *gstgtkbasesink_class;
|
||||
|
||||
gobject_class = (GObjectClass *) klass;
|
||||
gstelement_class = (GstElementClass *) klass;
|
||||
gstbasesink_class = (GstBaseSinkClass *) klass;
|
||||
gstgtkbasesink_class = (GstGtkBaseSinkClass *) klass;
|
||||
|
||||
gobject_class->finalize = gst_gtk_gl_sink_finalize;
|
||||
|
||||
gstbasesink_class->query = gst_gtk_gl_sink_query;
|
||||
gstbasesink_class->propose_allocation = gst_gtk_gl_sink_propose_allocation;
|
||||
gstbasesink_class->start = gst_gtk_gl_sink_start;
|
||||
gstbasesink_class->stop = gst_gtk_gl_sink_stop;
|
||||
gstbasesink_class->get_caps = gst_gtk_gl_sink_get_caps;
|
||||
|
||||
gstgtkbasesink_class->create_widget = gtk_gst_gl_widget_new;
|
||||
gstgtkbasesink_class->window_title = GTKCONFIG_NAME " GL Renderer";
|
||||
|
||||
gst_element_class_set_metadata (gstelement_class,
|
||||
GTKCONFIG_NAME " GL Video Sink",
|
||||
"Sink/Video", "A video sink that renders to a GtkWidget using OpenGL",
|
||||
"Matthew Waters <matthew@centricular.com>, "
|
||||
"Rafał Dzięgiel <rafostar.github@gmail.com>");
|
||||
|
||||
gst_element_class_add_static_pad_template (gstelement_class,
|
||||
&gst_gtk_gl_sink_template);
|
||||
}
|
||||
|
||||
static void
|
||||
gst_gtk_gl_sink_init (GstGtkGLSink * gtk_sink)
|
||||
{
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gst_gtk_gl_sink_query (GstBaseSink * bsink, GstQuery * query)
|
||||
{
|
||||
GstGtkGLSink *gtk_sink = GST_GTK_GL_SINK (bsink);
|
||||
gboolean res = FALSE;
|
||||
|
||||
switch (GST_QUERY_TYPE (query)) {
|
||||
case GST_QUERY_CONTEXT:
|
||||
{
|
||||
if (gst_gl_handle_context_query ((GstElement *) gtk_sink, query,
|
||||
gtk_sink->display, gtk_sink->context, gtk_sink->gtk_context))
|
||||
return TRUE;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
res = GST_BASE_SINK_CLASS (parent_class)->query (bsink, query);
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static void
|
||||
destroy_cb (GtkWidget * widget, GstGtkGLSink * gtk_sink)
|
||||
{
|
||||
if (gtk_sink->widget_destroy_sig_handler) {
|
||||
g_signal_handler_disconnect (widget, gtk_sink->widget_destroy_sig_handler);
|
||||
gtk_sink->widget_destroy_sig_handler = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gst_gtk_gl_sink_start (GstBaseSink * bsink)
|
||||
{
|
||||
GstGtkBaseSink *base_sink = GST_GTK_BASE_SINK (bsink);
|
||||
GstGtkGLSink *gtk_sink = GST_GTK_GL_SINK (bsink);
|
||||
GtkGstGLWidget *gst_widget;
|
||||
|
||||
if (!GST_BASE_SINK_CLASS (parent_class)->start (bsink))
|
||||
return FALSE;
|
||||
|
||||
/* After this point, gtk_sink->widget will always be set */
|
||||
gst_widget = GTK_GST_GL_WIDGET (base_sink->widget);
|
||||
|
||||
if (!gtk_sink->widget_destroy_sig_handler) {
|
||||
gtk_sink->widget_destroy_sig_handler =
|
||||
g_signal_connect (gst_widget, "destroy", G_CALLBACK (destroy_cb),
|
||||
gtk_sink);
|
||||
}
|
||||
|
||||
if (!gtk_gst_gl_widget_init_winsys (gst_widget)) {
|
||||
GST_ELEMENT_ERROR (bsink, RESOURCE, NOT_FOUND, ("%s",
|
||||
"Failed to initialize OpenGL with GTK"), (NULL));
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (!gtk_sink->display)
|
||||
gtk_sink->display = gtk_gst_gl_widget_get_display (gst_widget);
|
||||
if (!gtk_sink->context)
|
||||
gtk_sink->context = gtk_gst_gl_widget_get_context (gst_widget);
|
||||
if (!gtk_sink->gtk_context)
|
||||
gtk_sink->gtk_context = gtk_gst_gl_widget_get_gtk_context (gst_widget);
|
||||
|
||||
if (!gtk_sink->display || !gtk_sink->context || !gtk_sink->gtk_context) {
|
||||
GST_ELEMENT_ERROR (bsink, RESOURCE, NOT_FOUND, ("%s",
|
||||
"Failed to retrieve OpenGL context from GTK"), (NULL));
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
gst_gl_element_propagate_display_context (GST_ELEMENT (bsink),
|
||||
gtk_sink->display);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gst_gtk_gl_sink_stop (GstBaseSink * bsink)
|
||||
{
|
||||
GstGtkGLSink *gtk_sink = GST_GTK_GL_SINK (bsink);
|
||||
GstGtkBaseSink *base_sink = GST_GTK_BASE_SINK (bsink);
|
||||
|
||||
if (gtk_sink->display) {
|
||||
gst_object_unref (gtk_sink->display);
|
||||
gtk_sink->display = NULL;
|
||||
}
|
||||
|
||||
if (gtk_sink->context) {
|
||||
gst_object_unref (gtk_sink->context);
|
||||
gtk_sink->context = NULL;
|
||||
}
|
||||
|
||||
if (gtk_sink->gtk_context) {
|
||||
gst_object_unref (gtk_sink->gtk_context);
|
||||
gtk_sink->gtk_context = NULL;
|
||||
}
|
||||
|
||||
return GST_BASE_SINK_CLASS (parent_class)->stop (bsink);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gst_gtk_gl_sink_propose_allocation (GstBaseSink * bsink, GstQuery * query)
|
||||
{
|
||||
GstGtkGLSink *gtk_sink = GST_GTK_GL_SINK (bsink);
|
||||
GstBufferPool *pool = NULL;
|
||||
GstStructure *config;
|
||||
GstCaps *caps;
|
||||
GstVideoInfo info;
|
||||
guint size;
|
||||
gboolean need_pool;
|
||||
GstStructure *allocation_meta = NULL;
|
||||
gint display_width, display_height;
|
||||
|
||||
if (!gtk_sink->display || !gtk_sink->context)
|
||||
return FALSE;
|
||||
|
||||
gst_query_parse_allocation (query, &caps, &need_pool);
|
||||
|
||||
if (caps == NULL)
|
||||
goto no_caps;
|
||||
|
||||
if (!gst_video_info_from_caps (&info, caps))
|
||||
goto invalid_caps;
|
||||
|
||||
/* the normal size of a frame */
|
||||
size = info.size;
|
||||
|
||||
if (need_pool) {
|
||||
GST_DEBUG_OBJECT (gtk_sink, "create new pool");
|
||||
pool = gst_gl_buffer_pool_new (gtk_sink->context);
|
||||
|
||||
config = gst_buffer_pool_get_config (pool);
|
||||
gst_buffer_pool_config_set_params (config, caps, size, 0, 0);
|
||||
gst_buffer_pool_config_add_option (config,
|
||||
GST_BUFFER_POOL_OPTION_GL_SYNC_META);
|
||||
|
||||
if (!gst_buffer_pool_set_config (pool, config))
|
||||
goto config_failed;
|
||||
}
|
||||
|
||||
/* we need at least 2 buffer because we hold on to the last one */
|
||||
gst_query_add_allocation_pool (query, pool, size, 2, 0);
|
||||
if (pool)
|
||||
gst_object_unref (pool);
|
||||
|
||||
GST_OBJECT_LOCK (gtk_sink);
|
||||
display_width = gtk_sink->display_width;
|
||||
display_height = gtk_sink->display_height;
|
||||
GST_OBJECT_UNLOCK (gtk_sink);
|
||||
|
||||
if (display_width != 0 && display_height != 0) {
|
||||
GST_DEBUG_OBJECT (gtk_sink, "sending alloc query with size %dx%d",
|
||||
display_width, display_height);
|
||||
allocation_meta = gst_structure_new ("GstVideoOverlayCompositionMeta",
|
||||
"width", G_TYPE_UINT, display_width,
|
||||
"height", G_TYPE_UINT, display_height, NULL);
|
||||
}
|
||||
|
||||
gst_query_add_allocation_meta (query,
|
||||
GST_VIDEO_OVERLAY_COMPOSITION_META_API_TYPE, allocation_meta);
|
||||
|
||||
if (allocation_meta)
|
||||
gst_structure_free (allocation_meta);
|
||||
|
||||
/* we also support various metadata */
|
||||
gst_query_add_allocation_meta (query, GST_VIDEO_META_API_TYPE, 0);
|
||||
|
||||
if (gtk_sink->context->gl_vtable->FenceSync)
|
||||
gst_query_add_allocation_meta (query, GST_GL_SYNC_META_API_TYPE, 0);
|
||||
|
||||
return TRUE;
|
||||
|
||||
/* ERRORS */
|
||||
no_caps:
|
||||
{
|
||||
GST_DEBUG_OBJECT (bsink, "no caps specified");
|
||||
return FALSE;
|
||||
}
|
||||
invalid_caps:
|
||||
{
|
||||
GST_DEBUG_OBJECT (bsink, "invalid caps specified");
|
||||
return FALSE;
|
||||
}
|
||||
config_failed:
|
||||
{
|
||||
GST_DEBUG_OBJECT (bsink, "failed setting config");
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
static GstCaps *
|
||||
gst_gtk_gl_sink_get_caps (GstBaseSink * bsink, GstCaps * filter)
|
||||
{
|
||||
GstCaps *tmp = NULL;
|
||||
GstCaps *result = NULL;
|
||||
|
||||
tmp = gst_pad_get_pad_template_caps (GST_BASE_SINK_PAD (bsink));
|
||||
|
||||
if (filter) {
|
||||
GST_DEBUG_OBJECT (bsink, "intersecting with filter caps %" GST_PTR_FORMAT,
|
||||
filter);
|
||||
|
||||
result = gst_caps_intersect_full (filter, tmp, GST_CAPS_INTERSECT_FIRST);
|
||||
gst_caps_unref (tmp);
|
||||
} else {
|
||||
result = tmp;
|
||||
}
|
||||
|
||||
result = gst_gl_overlay_compositor_add_caps (result);
|
||||
|
||||
GST_DEBUG_OBJECT (bsink, "returning caps: %" GST_PTR_FORMAT, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static void
|
||||
gst_gtk_gl_sink_finalize (GObject * object)
|
||||
{
|
||||
GstGtkGLSink *gtk_sink = GST_GTK_GL_SINK (object);
|
||||
GstGtkBaseSink *base_sink = GST_GTK_BASE_SINK (object);
|
||||
|
||||
if (gtk_sink->widget_destroy_sig_handler) {
|
||||
g_signal_handler_disconnect (base_sink->widget,
|
||||
gtk_sink->widget_destroy_sig_handler);
|
||||
gtk_sink->widget_destroy_sig_handler = 0;
|
||||
}
|
||||
|
||||
G_OBJECT_CLASS (parent_class)->finalize (object);
|
||||
}
|
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* GStreamer
|
||||
* Copyright (C) 2015 Matthew Waters <matthew@centricular.com>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 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
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __GST_GTK_GL_SINK_H__
|
||||
#define __GST_GTK_GL_SINK_H__
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
#include <gst/gst.h>
|
||||
#include <gst/video/gstvideosink.h>
|
||||
#include <gst/video/video.h>
|
||||
#include <gst/gl/gl.h>
|
||||
|
||||
#include "gstgtkbasesink.h"
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
#define GST_TYPE_GTK_GL_SINK (gst_gtk_gl_sink_get_type ())
|
||||
G_DECLARE_FINAL_TYPE (GstGtkGLSink, gst_gtk_gl_sink, GST, GTK_GL_SINK,
|
||||
GstGtkBaseSink);
|
||||
|
||||
/**
|
||||
* GstGtkGLSink:
|
||||
*
|
||||
* Opaque #GstGtkGLSink object
|
||||
*/
|
||||
struct _GstGtkGLSink
|
||||
{
|
||||
/* <private> */
|
||||
GstGtkBaseSink parent;
|
||||
|
||||
GstGLDisplay *display;
|
||||
GstGLContext *context;
|
||||
GstGLContext *gtk_context;
|
||||
|
||||
GstGLUpload *upload;
|
||||
GstBuffer *uploaded_buffer;
|
||||
|
||||
/* read/write with object lock */
|
||||
gint display_width;
|
||||
gint display_height;
|
||||
|
||||
gulong widget_destroy_sig_handler;
|
||||
};
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif /* __GST_GTK_GL_SINK_H__ */
|
1100
lib/gst/clapper/gtk4/gtkclapperglwidget.c
Normal file
1100
lib/gst/clapper/gtk4/gtkclapperglwidget.c
Normal file
File diff suppressed because it is too large
Load Diff
107
lib/gst/clapper/gtk4/gtkclapperglwidget.h
Normal file
107
lib/gst/clapper/gtk4/gtkclapperglwidget.h
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* GStreamer
|
||||
* Copyright (C) 2015 Matthew Waters <matthew@centricular.com>
|
||||
* Copyright (C) 2020 Rafał Dzięgiel <rafostar.github@gmail.com>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 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
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __GTK_CLAPPER_GL_WIDGET_H__
|
||||
#define __GTK_CLAPPER_GL_WIDGET_H__
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
#include <gst/gst.h>
|
||||
#include <gst/video/video.h>
|
||||
#include <gst/gl/gl.h>
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
GType gtk_clapper_gl_widget_get_type (void);
|
||||
#define GTK_TYPE_CLAPPER_GL_WIDGET (gtk_clapper_gl_widget_get_type())
|
||||
#define GTK_CLAPPER_GL_WIDGET(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),GTK_TYPE_CLAPPER_GL_WIDGET,GtkClapperGLWidget))
|
||||
#define GTK_CLAPPER_GL_WIDGET_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass),GTK_TYPE_CLAPPER_GL_WIDGET,GtkClapperGLWidgetClass))
|
||||
#define GTK_IS_CLAPPER_GL_WIDGET(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj),GTK_TYPE_CLAPPER_GL_WIDGET))
|
||||
#define GTK_IS_CLAPPER_GL_WIDGET_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass),GTK_TYPE_CLAPPER_GL_WIDGET))
|
||||
#define GTK_CLAPPER_GL_WIDGET_CAST(obj) ((GtkClapperGLWidget*)(obj))
|
||||
#define GTK_CLAPPER_GL_WIDGET_LOCK(w) g_mutex_lock(&((GtkClapperGLWidget*)(w))->lock)
|
||||
#define GTK_CLAPPER_GL_WIDGET_UNLOCK(w) g_mutex_unlock(&((GtkClapperGLWidget*)(w))->lock)
|
||||
|
||||
typedef struct _GtkClapperGLWidget GtkClapperGLWidget;
|
||||
typedef struct _GtkClapperGLWidgetClass GtkClapperGLWidgetClass;
|
||||
typedef struct _GtkClapperGLWidgetPrivate GtkClapperGLWidgetPrivate;
|
||||
|
||||
struct _GtkClapperGLWidget
|
||||
{
|
||||
/* <private> */
|
||||
GtkGLArea parent;
|
||||
GtkClapperGLWidgetPrivate *priv;
|
||||
|
||||
/* properties */
|
||||
gboolean force_aspect_ratio;
|
||||
gint par_n, par_d;
|
||||
gboolean ignore_textures;
|
||||
|
||||
gint display_width;
|
||||
gint display_height;
|
||||
|
||||
/* Widget dimensions */
|
||||
gint scaled_width;
|
||||
gint scaled_height;
|
||||
|
||||
gboolean negotiated;
|
||||
GstBuffer *pending_buffer;
|
||||
GstBuffer *buffer;
|
||||
GstVideoInfo v_info;
|
||||
|
||||
/* resize */
|
||||
gboolean pending_resize;
|
||||
GstVideoInfo pending_v_info;
|
||||
guint display_ratio_num;
|
||||
guint display_ratio_den;
|
||||
|
||||
/*< private >*/
|
||||
GMutex lock;
|
||||
GWeakRef element;
|
||||
|
||||
/* event controllers */
|
||||
GtkEventController *key_controller;
|
||||
GtkEventController *motion_controller;
|
||||
GtkGesture *click_gesture;
|
||||
|
||||
/* Pending draw idles callback */
|
||||
guint draw_id;
|
||||
};
|
||||
|
||||
struct _GtkClapperGLWidgetClass
|
||||
{
|
||||
GtkGLAreaClass parent_class;
|
||||
};
|
||||
|
||||
/* API */
|
||||
gboolean gtk_clapper_gl_widget_set_format (GtkClapperGLWidget * widget, GstVideoInfo * v_info);
|
||||
void gtk_clapper_gl_widget_set_buffer (GtkClapperGLWidget * widget, GstBuffer * buffer);
|
||||
void gtk_clapper_gl_widget_set_element (GtkClapperGLWidget * widget, GstElement * element);
|
||||
|
||||
GtkWidget * gtk_clapper_gl_widget_new (void);
|
||||
|
||||
gboolean gtk_clapper_gl_widget_init_winsys (GtkClapperGLWidget * widget);
|
||||
GstGLDisplay * gtk_clapper_gl_widget_get_display (GtkClapperGLWidget * widget);
|
||||
GstGLContext * gtk_clapper_gl_widget_get_context (GtkClapperGLWidget * widget);
|
||||
GstGLContext * gtk_clapper_gl_widget_get_gtk_context (GtkClapperGLWidget * widget);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif /* __GTK_CLAPPER_GL_WIDGET_H__ */
|
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* GStreamer
|
||||
* Copyright (C) 2020 Rafał Dzięgiel <rafostar.github@gmail.com>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 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
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#if defined(BUILD_FOR_GTK4)
|
||||
#define GTKCONFIG_PLUGIN gtk4
|
||||
#define GTKCONFIG_NAME "GTK4"
|
||||
#define GTKCONFIG_SINK "gtk4sink"
|
||||
#define GTKCONFIG_GLSINK "gtk4glsink"
|
||||
#else
|
||||
#define GTKCONFIG_PLUGIN gtk
|
||||
#define GTKCONFIG_NAME "GTK"
|
||||
#define GTKCONFIG_SINK "gtksink"
|
||||
#define GTKCONFIG_GLSINK "gtkglsink"
|
||||
#endif
|
@@ -1,619 +0,0 @@
|
||||
/*
|
||||
* GStreamer
|
||||
* Copyright (C) 2015 Matthew Waters <matthew@centricular.com>
|
||||
* Copyright (C) 2020 Rafał Dzięgiel <rafostar.github@gmail.com>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 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
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#include "gtkgstbasewidget.h"
|
||||
|
||||
GST_DEBUG_CATEGORY (gst_debug_gtk_base_widget);
|
||||
#define GST_CAT_DEFAULT gst_debug_gtk_base_widget
|
||||
|
||||
#define DEFAULT_FORCE_ASPECT_RATIO TRUE
|
||||
#define DEFAULT_PAR_N 0
|
||||
#define DEFAULT_PAR_D 1
|
||||
#define DEFAULT_IGNORE_ALPHA TRUE
|
||||
#define DEFAULT_IGNORE_TEXTURES FALSE
|
||||
|
||||
enum
|
||||
{
|
||||
PROP_0,
|
||||
PROP_FORCE_ASPECT_RATIO,
|
||||
PROP_PIXEL_ASPECT_RATIO,
|
||||
PROP_IGNORE_ALPHA,
|
||||
PROP_IGNORE_TEXTURES,
|
||||
};
|
||||
|
||||
static void
|
||||
gtk_gst_base_widget_get_preferred_width (GtkWidget * widget, gint * min,
|
||||
gint * natural)
|
||||
{
|
||||
GtkGstBaseWidget *gst_widget = (GtkGstBaseWidget *) widget;
|
||||
gint video_width = gst_widget->display_width;
|
||||
|
||||
if (!gst_widget->negotiated)
|
||||
video_width = 10;
|
||||
|
||||
if (min)
|
||||
*min = 1;
|
||||
if (natural)
|
||||
*natural = video_width;
|
||||
}
|
||||
|
||||
static void
|
||||
gtk_gst_base_widget_get_preferred_height (GtkWidget * widget, gint * min,
|
||||
gint * natural)
|
||||
{
|
||||
GtkGstBaseWidget *gst_widget = (GtkGstBaseWidget *) widget;
|
||||
gint video_height = gst_widget->display_height;
|
||||
|
||||
if (!gst_widget->negotiated)
|
||||
video_height = 10;
|
||||
|
||||
if (min)
|
||||
*min = 1;
|
||||
if (natural)
|
||||
*natural = video_height;
|
||||
}
|
||||
|
||||
#if defined(BUILD_FOR_GTK4)
|
||||
static void
|
||||
gtk_gst_base_widget_measure (GtkWidget * widget, GtkOrientation orientation,
|
||||
gint for_size, gint * min, gint * natural,
|
||||
gint * minimum_baseline, gint * natural_baseline)
|
||||
{
|
||||
if (orientation == GTK_ORIENTATION_HORIZONTAL)
|
||||
gtk_gst_base_widget_get_preferred_width (widget, min, natural);
|
||||
else
|
||||
gtk_gst_base_widget_get_preferred_height (widget, min, natural);
|
||||
|
||||
*minimum_baseline = -1;
|
||||
*natural_baseline = -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
static void
|
||||
gtk_gst_base_widget_set_property (GObject * object, guint prop_id,
|
||||
const GValue * value, GParamSpec * pspec)
|
||||
{
|
||||
GtkGstBaseWidget *gtk_widget = GTK_GST_BASE_WIDGET (object);
|
||||
|
||||
switch (prop_id) {
|
||||
case PROP_FORCE_ASPECT_RATIO:
|
||||
gtk_widget->force_aspect_ratio = g_value_get_boolean (value);
|
||||
break;
|
||||
case PROP_PIXEL_ASPECT_RATIO:
|
||||
gtk_widget->par_n = gst_value_get_fraction_numerator (value);
|
||||
gtk_widget->par_d = gst_value_get_fraction_denominator (value);
|
||||
break;
|
||||
case PROP_IGNORE_ALPHA:
|
||||
gtk_widget->ignore_alpha = g_value_get_boolean (value);
|
||||
break;
|
||||
case PROP_IGNORE_TEXTURES:
|
||||
gtk_widget->ignore_textures = g_value_get_boolean (value);
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
gtk_gst_base_widget_get_property (GObject * object, guint prop_id,
|
||||
GValue * value, GParamSpec * pspec)
|
||||
{
|
||||
GtkGstBaseWidget *gtk_widget = GTK_GST_BASE_WIDGET (object);
|
||||
|
||||
switch (prop_id) {
|
||||
case PROP_FORCE_ASPECT_RATIO:
|
||||
g_value_set_boolean (value, gtk_widget->force_aspect_ratio);
|
||||
break;
|
||||
case PROP_PIXEL_ASPECT_RATIO:
|
||||
gst_value_set_fraction (value, gtk_widget->par_n, gtk_widget->par_d);
|
||||
break;
|
||||
case PROP_IGNORE_ALPHA:
|
||||
g_value_set_boolean (value, gtk_widget->ignore_alpha);
|
||||
break;
|
||||
case PROP_IGNORE_TEXTURES:
|
||||
g_value_set_boolean (value, gtk_widget->ignore_textures);
|
||||
break;
|
||||
default:
|
||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean
|
||||
_calculate_par (GtkGstBaseWidget * widget, GstVideoInfo * info)
|
||||
{
|
||||
gboolean ok;
|
||||
gint width, height;
|
||||
gint par_n, par_d;
|
||||
gint display_par_n, display_par_d;
|
||||
|
||||
width = GST_VIDEO_INFO_WIDTH (info);
|
||||
height = GST_VIDEO_INFO_HEIGHT (info);
|
||||
|
||||
par_n = GST_VIDEO_INFO_PAR_N (info);
|
||||
par_d = GST_VIDEO_INFO_PAR_D (info);
|
||||
|
||||
if (!par_n)
|
||||
par_n = 1;
|
||||
|
||||
/* get display's PAR */
|
||||
if (widget->par_n != 0 && widget->par_d != 0) {
|
||||
display_par_n = widget->par_n;
|
||||
display_par_d = widget->par_d;
|
||||
} else {
|
||||
display_par_n = 1;
|
||||
display_par_d = 1;
|
||||
}
|
||||
|
||||
|
||||
ok = gst_video_calculate_display_ratio (&widget->display_ratio_num,
|
||||
&widget->display_ratio_den, width, height, par_n, par_d, display_par_n,
|
||||
display_par_d);
|
||||
|
||||
if (ok) {
|
||||
GST_LOG ("PAR: %u/%u DAR:%u/%u", par_n, par_d, display_par_n,
|
||||
display_par_d);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static void
|
||||
_apply_par (GtkGstBaseWidget * widget)
|
||||
{
|
||||
guint display_ratio_num, display_ratio_den;
|
||||
gint width, height;
|
||||
|
||||
width = GST_VIDEO_INFO_WIDTH (&widget->v_info);
|
||||
height = GST_VIDEO_INFO_HEIGHT (&widget->v_info);
|
||||
|
||||
display_ratio_num = widget->display_ratio_num;
|
||||
display_ratio_den = widget->display_ratio_den;
|
||||
|
||||
if (height % display_ratio_den == 0) {
|
||||
GST_DEBUG ("keeping video height");
|
||||
widget->display_width = (guint)
|
||||
gst_util_uint64_scale_int (height, display_ratio_num,
|
||||
display_ratio_den);
|
||||
widget->display_height = height;
|
||||
} else if (width % display_ratio_num == 0) {
|
||||
GST_DEBUG ("keeping video width");
|
||||
widget->display_width = width;
|
||||
widget->display_height = (guint)
|
||||
gst_util_uint64_scale_int (width, display_ratio_den, display_ratio_num);
|
||||
} else {
|
||||
GST_DEBUG ("approximating while keeping video height");
|
||||
widget->display_width = (guint)
|
||||
gst_util_uint64_scale_int (height, display_ratio_num,
|
||||
display_ratio_den);
|
||||
widget->display_height = height;
|
||||
}
|
||||
|
||||
GST_DEBUG ("scaling to %dx%d", widget->display_width, widget->display_height);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
_queue_draw (GtkGstBaseWidget * widget)
|
||||
{
|
||||
GTK_GST_BASE_WIDGET_LOCK (widget);
|
||||
widget->draw_id = 0;
|
||||
|
||||
if (widget->pending_resize) {
|
||||
widget->pending_resize = FALSE;
|
||||
|
||||
widget->v_info = widget->pending_v_info;
|
||||
widget->negotiated = TRUE;
|
||||
|
||||
_apply_par (widget);
|
||||
|
||||
gtk_widget_queue_resize (GTK_WIDGET (widget));
|
||||
} else {
|
||||
gtk_widget_queue_draw (GTK_WIDGET (widget));
|
||||
}
|
||||
|
||||
GTK_GST_BASE_WIDGET_UNLOCK (widget);
|
||||
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static const gchar *
|
||||
_gdk_key_to_navigation_string (guint keyval)
|
||||
{
|
||||
/* TODO: expand */
|
||||
switch (keyval) {
|
||||
#define KEY(key) case GDK_KEY_ ## key: return G_STRINGIFY(key)
|
||||
KEY (Up);
|
||||
KEY (Down);
|
||||
KEY (Left);
|
||||
KEY (Right);
|
||||
KEY (Home);
|
||||
KEY (End);
|
||||
#undef KEY
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static GdkEvent *
|
||||
_get_current_event (GtkEventController * controller)
|
||||
{
|
||||
#if defined(BUILD_FOR_GTK4)
|
||||
return gtk_event_controller_get_current_event (controller);
|
||||
#else
|
||||
return gtk_get_current_event ();
|
||||
#endif
|
||||
}
|
||||
|
||||
static void
|
||||
_gdk_event_free (GdkEvent * event)
|
||||
{
|
||||
#if !defined(BUILD_FOR_GTK4)
|
||||
if (event)
|
||||
gdk_event_free (event);
|
||||
#endif
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gtk_gst_base_widget_key_event (GtkEventControllerKey * key_controller,
|
||||
guint keyval, guint keycode, GdkModifierType state)
|
||||
{
|
||||
GtkEventController *controller = GTK_EVENT_CONTROLLER (key_controller);
|
||||
GtkWidget *widget = gtk_event_controller_get_widget (controller);
|
||||
GtkGstBaseWidget *base_widget = GTK_GST_BASE_WIDGET (widget);
|
||||
GstElement *element;
|
||||
|
||||
if ((element = g_weak_ref_get (&base_widget->element))) {
|
||||
if (GST_IS_NAVIGATION (element)) {
|
||||
GdkEvent *event = _get_current_event (controller);
|
||||
const gchar *str = _gdk_key_to_navigation_string (keyval);
|
||||
|
||||
if (str) {
|
||||
const gchar *key_type =
|
||||
gdk_event_get_event_type (event) ==
|
||||
GDK_KEY_PRESS ? "key-press" : "key-release";
|
||||
gst_navigation_send_key_event (GST_NAVIGATION (element), key_type, str);
|
||||
}
|
||||
_gdk_event_free (event);
|
||||
}
|
||||
g_object_unref (element);
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static void
|
||||
_fit_stream_to_allocated_size (GtkGstBaseWidget * base_widget,
|
||||
GtkAllocation * allocation, GstVideoRectangle * result)
|
||||
{
|
||||
if (base_widget->force_aspect_ratio) {
|
||||
GstVideoRectangle src, dst;
|
||||
|
||||
src.x = 0;
|
||||
src.y = 0;
|
||||
src.w = base_widget->display_width;
|
||||
src.h = base_widget->display_height;
|
||||
|
||||
dst.x = 0;
|
||||
dst.y = 0;
|
||||
dst.w = allocation->width;
|
||||
dst.h = allocation->height;
|
||||
|
||||
gst_video_sink_center_rect (src, dst, result, TRUE);
|
||||
} else {
|
||||
result->x = 0;
|
||||
result->y = 0;
|
||||
result->w = allocation->width;
|
||||
result->h = allocation->height;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
_display_size_to_stream_size (GtkGstBaseWidget * base_widget, gdouble x,
|
||||
gdouble y, gdouble * stream_x, gdouble * stream_y)
|
||||
{
|
||||
gdouble stream_width, stream_height;
|
||||
GtkAllocation allocation;
|
||||
GstVideoRectangle result;
|
||||
|
||||
gtk_widget_get_allocation (GTK_WIDGET (base_widget), &allocation);
|
||||
_fit_stream_to_allocated_size (base_widget, &allocation, &result);
|
||||
|
||||
stream_width = (gdouble) GST_VIDEO_INFO_WIDTH (&base_widget->v_info);
|
||||
stream_height = (gdouble) GST_VIDEO_INFO_HEIGHT (&base_widget->v_info);
|
||||
|
||||
/* from display coordinates to stream coordinates */
|
||||
if (result.w > 0)
|
||||
*stream_x = (x - result.x) / result.w * stream_width;
|
||||
else
|
||||
*stream_x = 0.;
|
||||
|
||||
/* clip to stream size */
|
||||
if (*stream_x < 0.)
|
||||
*stream_x = 0.;
|
||||
if (*stream_x > GST_VIDEO_INFO_WIDTH (&base_widget->v_info))
|
||||
*stream_x = GST_VIDEO_INFO_WIDTH (&base_widget->v_info);
|
||||
|
||||
/* same for y-axis */
|
||||
if (result.h > 0)
|
||||
*stream_y = (y - result.y) / result.h * stream_height;
|
||||
else
|
||||
*stream_y = 0.;
|
||||
|
||||
if (*stream_y < 0.)
|
||||
*stream_y = 0.;
|
||||
if (*stream_y > GST_VIDEO_INFO_HEIGHT (&base_widget->v_info))
|
||||
*stream_y = GST_VIDEO_INFO_HEIGHT (&base_widget->v_info);
|
||||
|
||||
GST_TRACE ("transform %fx%f into %fx%f", x, y, *stream_x, *stream_y);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gtk_gst_base_widget_button_event (
|
||||
#if defined(BUILD_FOR_GTK4)
|
||||
GtkGestureClick * gesture,
|
||||
#else
|
||||
GtkGestureMultiPress * gesture,
|
||||
#endif
|
||||
gint n_press, gdouble x, gdouble y)
|
||||
{
|
||||
GtkEventController *controller = GTK_EVENT_CONTROLLER (gesture);
|
||||
GtkWidget *widget = gtk_event_controller_get_widget (controller);
|
||||
GtkGstBaseWidget *base_widget = GTK_GST_BASE_WIDGET (widget);
|
||||
GstElement *element;
|
||||
|
||||
if ((element = g_weak_ref_get (&base_widget->element))) {
|
||||
if (GST_IS_NAVIGATION (element)) {
|
||||
GdkEvent *event = _get_current_event (controller);
|
||||
const gchar *key_type =
|
||||
gdk_event_get_event_type (event) == GDK_BUTTON_PRESS
|
||||
? "mouse-button-press" : "mouse-button-release";
|
||||
gdouble stream_x, stream_y;
|
||||
#if !defined(BUILD_FOR_GTK4)
|
||||
guint button;
|
||||
gdk_event_get_button (event, &button);
|
||||
#endif
|
||||
|
||||
_display_size_to_stream_size (base_widget, x, y, &stream_x, &stream_y);
|
||||
|
||||
gst_navigation_send_mouse_event (GST_NAVIGATION (element), key_type,
|
||||
#if defined(BUILD_FOR_GTK4)
|
||||
/* Gesture is set to ignore other buttons so we do not have to check */
|
||||
GDK_BUTTON_PRIMARY,
|
||||
#else
|
||||
button,
|
||||
#endif
|
||||
stream_x, stream_y);
|
||||
|
||||
_gdk_event_free (event);
|
||||
}
|
||||
g_object_unref (element);
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gtk_gst_base_widget_motion_event (GtkEventControllerMotion * motion_controller,
|
||||
gdouble x, gdouble y)
|
||||
{
|
||||
GtkEventController *controller = GTK_EVENT_CONTROLLER (motion_controller);
|
||||
GtkWidget *widget = gtk_event_controller_get_widget (controller);
|
||||
GtkGstBaseWidget *base_widget = GTK_GST_BASE_WIDGET (widget);
|
||||
GstElement *element;
|
||||
|
||||
if ((element = g_weak_ref_get (&base_widget->element))) {
|
||||
if (GST_IS_NAVIGATION (element)) {
|
||||
gdouble stream_x, stream_y;
|
||||
|
||||
_display_size_to_stream_size (base_widget, x, y, &stream_x, &stream_y);
|
||||
|
||||
gst_navigation_send_mouse_event (GST_NAVIGATION (element), "mouse-move",
|
||||
0, stream_x, stream_y);
|
||||
}
|
||||
g_object_unref (element);
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
void
|
||||
gtk_gst_base_widget_class_init (GtkGstBaseWidgetClass * klass)
|
||||
{
|
||||
GObjectClass *gobject_klass = (GObjectClass *) klass;
|
||||
GtkWidgetClass *widget_klass = (GtkWidgetClass *) klass;
|
||||
|
||||
gobject_klass->set_property = gtk_gst_base_widget_set_property;
|
||||
gobject_klass->get_property = gtk_gst_base_widget_get_property;
|
||||
|
||||
g_object_class_install_property (gobject_klass, PROP_FORCE_ASPECT_RATIO,
|
||||
g_param_spec_boolean ("force-aspect-ratio",
|
||||
"Force aspect ratio",
|
||||
"When enabled, scaling will respect original aspect ratio",
|
||||
DEFAULT_FORCE_ASPECT_RATIO,
|
||||
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
||||
|
||||
g_object_class_install_property (gobject_klass, PROP_PIXEL_ASPECT_RATIO,
|
||||
gst_param_spec_fraction ("pixel-aspect-ratio", "Pixel Aspect Ratio",
|
||||
"The pixel aspect ratio of the device", DEFAULT_PAR_N, DEFAULT_PAR_D,
|
||||
G_MAXINT, 1, 1, 1, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
||||
|
||||
g_object_class_install_property (gobject_klass, PROP_IGNORE_ALPHA,
|
||||
g_param_spec_boolean ("ignore-alpha", "Ignore Alpha",
|
||||
"When enabled, alpha will be ignored and converted to black",
|
||||
DEFAULT_IGNORE_ALPHA, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
||||
|
||||
g_object_class_install_property (gobject_klass, PROP_IGNORE_TEXTURES,
|
||||
g_param_spec_boolean ("ignore-textures", "Ignore Textures",
|
||||
"When enabled, textures will be ignored and not drawn",
|
||||
DEFAULT_IGNORE_TEXTURES, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
||||
|
||||
#if defined(BUILD_FOR_GTK4)
|
||||
widget_klass->measure = gtk_gst_base_widget_measure;
|
||||
#else
|
||||
widget_klass->get_preferred_width = gtk_gst_base_widget_get_preferred_width;
|
||||
widget_klass->get_preferred_height = gtk_gst_base_widget_get_preferred_height;
|
||||
#endif
|
||||
|
||||
GST_DEBUG_CATEGORY_INIT (gst_debug_gtk_base_widget, "gtkbasewidget", 0,
|
||||
"GTK Video Base Widget");
|
||||
}
|
||||
|
||||
void
|
||||
gtk_gst_base_widget_init (GtkGstBaseWidget * widget)
|
||||
{
|
||||
widget->force_aspect_ratio = DEFAULT_FORCE_ASPECT_RATIO;
|
||||
widget->par_n = DEFAULT_PAR_N;
|
||||
widget->par_d = DEFAULT_PAR_D;
|
||||
widget->ignore_alpha = DEFAULT_IGNORE_ALPHA;
|
||||
widget->ignore_textures = DEFAULT_IGNORE_TEXTURES;
|
||||
|
||||
gst_video_info_init (&widget->v_info);
|
||||
gst_video_info_init (&widget->pending_v_info);
|
||||
|
||||
g_weak_ref_init (&widget->element, NULL);
|
||||
g_mutex_init (&widget->lock);
|
||||
|
||||
widget->key_controller = gtk_event_controller_key_new (
|
||||
#if !defined(BUILD_FOR_GTK4)
|
||||
GTK_WIDGET (widget)
|
||||
#endif
|
||||
);
|
||||
g_signal_connect (widget->key_controller, "key-pressed",
|
||||
G_CALLBACK (gtk_gst_base_widget_key_event), NULL);
|
||||
g_signal_connect (widget->key_controller, "key-released",
|
||||
G_CALLBACK (gtk_gst_base_widget_key_event), NULL);
|
||||
|
||||
widget->motion_controller = gtk_event_controller_motion_new (
|
||||
#if !defined(BUILD_FOR_GTK4)
|
||||
GTK_WIDGET (widget)
|
||||
#endif
|
||||
);
|
||||
g_signal_connect (widget->motion_controller, "motion",
|
||||
G_CALLBACK (gtk_gst_base_widget_motion_event), NULL);
|
||||
|
||||
widget->click_gesture =
|
||||
#if defined(BUILD_FOR_GTK4)
|
||||
gtk_gesture_click_new ();
|
||||
#else
|
||||
gtk_gesture_multi_press_new (GTK_WIDGET (widget));
|
||||
#endif
|
||||
g_signal_connect (widget->click_gesture, "pressed",
|
||||
G_CALLBACK (gtk_gst_base_widget_button_event), NULL);
|
||||
g_signal_connect (widget->click_gesture, "released",
|
||||
G_CALLBACK (gtk_gst_base_widget_button_event), NULL);
|
||||
|
||||
#if defined(BUILD_FOR_GTK4)
|
||||
/* Otherwise widget in grid will appear as a 1x1px
|
||||
* video which might be misleading for users */
|
||||
gtk_widget_set_hexpand (GTK_WIDGET (widget), TRUE);
|
||||
gtk_widget_set_vexpand (GTK_WIDGET (widget), TRUE);
|
||||
|
||||
gtk_widget_set_focusable (GTK_WIDGET (widget), TRUE);
|
||||
gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (widget->click_gesture),
|
||||
GDK_BUTTON_PRIMARY);
|
||||
|
||||
gtk_widget_add_controller (GTK_WIDGET (widget), widget->key_controller);
|
||||
gtk_widget_add_controller (GTK_WIDGET (widget), widget->motion_controller);
|
||||
gtk_widget_add_controller (GTK_WIDGET (widget),
|
||||
GTK_EVENT_CONTROLLER (widget->click_gesture));
|
||||
#endif
|
||||
|
||||
gtk_widget_set_can_focus (GTK_WIDGET (widget), TRUE);
|
||||
}
|
||||
|
||||
void
|
||||
gtk_gst_base_widget_finalize (GObject * object)
|
||||
{
|
||||
GtkGstBaseWidget *widget = GTK_GST_BASE_WIDGET (object);
|
||||
|
||||
/* GTK4 takes ownership of EventControllers
|
||||
* while GTK3 still needs manual unref */
|
||||
#if !defined(BUILD_FOR_GTK4)
|
||||
g_object_unref (widget->key_controller);
|
||||
g_object_unref (widget->motion_controller);
|
||||
g_object_unref (widget->click_gesture);
|
||||
#endif
|
||||
|
||||
gst_buffer_replace (&widget->pending_buffer, NULL);
|
||||
gst_buffer_replace (&widget->buffer, NULL);
|
||||
g_mutex_clear (&widget->lock);
|
||||
g_weak_ref_clear (&widget->element);
|
||||
|
||||
if (widget->draw_id)
|
||||
g_source_remove (widget->draw_id);
|
||||
}
|
||||
|
||||
void
|
||||
gtk_gst_base_widget_set_element (GtkGstBaseWidget * widget,
|
||||
GstElement * element)
|
||||
{
|
||||
g_weak_ref_set (&widget->element, element);
|
||||
}
|
||||
|
||||
gboolean
|
||||
gtk_gst_base_widget_set_format (GtkGstBaseWidget * widget,
|
||||
GstVideoInfo * v_info)
|
||||
{
|
||||
GTK_GST_BASE_WIDGET_LOCK (widget);
|
||||
|
||||
if (gst_video_info_is_equal (&widget->pending_v_info, v_info)) {
|
||||
GTK_GST_BASE_WIDGET_UNLOCK (widget);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (!_calculate_par (widget, v_info)) {
|
||||
GTK_GST_BASE_WIDGET_UNLOCK (widget);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
widget->pending_resize = TRUE;
|
||||
widget->pending_v_info = *v_info;
|
||||
|
||||
GTK_GST_BASE_WIDGET_UNLOCK (widget);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void
|
||||
gtk_gst_base_widget_set_buffer (GtkGstBaseWidget * widget, GstBuffer * buffer)
|
||||
{
|
||||
/* As we have no type, this is better then no check */
|
||||
g_return_if_fail (GTK_IS_WIDGET (widget));
|
||||
|
||||
GTK_GST_BASE_WIDGET_LOCK (widget);
|
||||
|
||||
gst_buffer_replace (&widget->pending_buffer, buffer);
|
||||
|
||||
if (!widget->draw_id) {
|
||||
widget->draw_id = g_idle_add_full (G_PRIORITY_DEFAULT,
|
||||
(GSourceFunc) _queue_draw, widget, NULL);
|
||||
}
|
||||
|
||||
GTK_GST_BASE_WIDGET_UNLOCK (widget);
|
||||
}
|
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* GStreamer
|
||||
* Copyright (C) 2015 Matthew Waters <matthew@centricular.com>
|
||||
* Copyright (C) 2020 Rafał Dzięgiel <rafostar.github@gmail.com>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 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
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __GTK_GST_BASE_WIDGET_H__
|
||||
#define __GTK_GST_BASE_WIDGET_H__
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
#include <gst/gst.h>
|
||||
#include <gst/video/video.h>
|
||||
|
||||
#if !defined(BUILD_FOR_GTK4)
|
||||
#include <gdk/gdk.h>
|
||||
#endif
|
||||
|
||||
#define GTK_GST_BASE_WIDGET(w) ((GtkGstBaseWidget *)(w))
|
||||
#define GTK_GST_BASE_WIDGET_CLASS(k) ((GtkGstBaseWidgetClass *)(k))
|
||||
#define GTK_GST_BASE_WIDGET_LOCK(w) g_mutex_lock(&((GtkGstBaseWidget*)(w))->lock)
|
||||
#define GTK_GST_BASE_WIDGET_UNLOCK(w) g_mutex_unlock(&((GtkGstBaseWidget*)(w))->lock)
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
typedef struct _GtkGstBaseWidget GtkGstBaseWidget;
|
||||
typedef struct _GtkGstBaseWidgetClass GtkGstBaseWidgetClass;
|
||||
|
||||
struct _GtkGstBaseWidget
|
||||
{
|
||||
union {
|
||||
GtkGLArea gl_area;
|
||||
} parent;
|
||||
|
||||
/* properties */
|
||||
gboolean force_aspect_ratio;
|
||||
gint par_n, par_d;
|
||||
gboolean ignore_alpha;
|
||||
gboolean ignore_textures;
|
||||
|
||||
gint display_width;
|
||||
gint display_height;
|
||||
|
||||
gboolean negotiated;
|
||||
GstBuffer *pending_buffer;
|
||||
GstBuffer *buffer;
|
||||
GstVideoInfo v_info;
|
||||
|
||||
/* resize */
|
||||
gboolean pending_resize;
|
||||
GstVideoInfo pending_v_info;
|
||||
guint display_ratio_num;
|
||||
guint display_ratio_den;
|
||||
|
||||
/*< private >*/
|
||||
GMutex lock;
|
||||
GWeakRef element;
|
||||
|
||||
/* event controllers */
|
||||
GtkEventController *key_controller;
|
||||
GtkEventController *motion_controller;
|
||||
GtkGesture *click_gesture;
|
||||
|
||||
/* Pending draw idles callback */
|
||||
guint draw_id;
|
||||
};
|
||||
|
||||
struct _GtkGstBaseWidgetClass
|
||||
{
|
||||
union {
|
||||
GtkGLAreaClass gl_area_class;
|
||||
} parent_class;
|
||||
};
|
||||
|
||||
/* For implementer */
|
||||
void gtk_gst_base_widget_class_init (GtkGstBaseWidgetClass * klass);
|
||||
void gtk_gst_base_widget_init (GtkGstBaseWidget * widget);
|
||||
|
||||
void gtk_gst_base_widget_finalize (GObject * object);
|
||||
|
||||
/* API */
|
||||
gboolean gtk_gst_base_widget_set_format (GtkGstBaseWidget * widget, GstVideoInfo * v_info);
|
||||
void gtk_gst_base_widget_set_buffer (GtkGstBaseWidget * widget, GstBuffer * buffer);
|
||||
void gtk_gst_base_widget_set_element (GtkGstBaseWidget * widget, GstElement * element);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif /* __GTK_GST_BASE_WIDGET_H__ */
|
@@ -1,604 +0,0 @@
|
||||
/*
|
||||
* GStreamer
|
||||
* Copyright (C) 2015 Matthew Waters <matthew@centricular.com>
|
||||
* Copyright (C) 2020 Rafał Dzięgiel <rafostar.github@gmail.com>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 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
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifdef HAVE_CONFIG_H
|
||||
#include "config.h"
|
||||
#endif
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#include "gtkgstglwidget.h"
|
||||
#include "gstgtkutils.h"
|
||||
#include <gst/gl/gstglfuncs.h>
|
||||
#include <gst/video/video.h>
|
||||
|
||||
#if GST_GL_HAVE_WINDOW_X11 && defined (GDK_WINDOWING_X11)
|
||||
#if defined(BUILD_FOR_GTK4)
|
||||
#include <gdk/x11/gdkx.h>
|
||||
#else
|
||||
#include <gdk/gdkx.h>
|
||||
#endif
|
||||
#include <gst/gl/x11/gstgldisplay_x11.h>
|
||||
#endif
|
||||
|
||||
#if GST_GL_HAVE_WINDOW_WAYLAND && defined (GDK_WINDOWING_WAYLAND)
|
||||
#if defined(BUILD_FOR_GTK4)
|
||||
#include <gdk/wayland/gdkwayland.h>
|
||||
#else
|
||||
#include <gdk/gdkwayland.h>
|
||||
#endif
|
||||
#include <gst/gl/wayland/gstgldisplay_wayland.h>
|
||||
#endif
|
||||
|
||||
/**
|
||||
* SECTION:gtkgstglwidget
|
||||
* @title: GtkGstGlWidget
|
||||
* @short_description: a #GtkGLArea that renders GStreamer video #GstBuffers
|
||||
* @see_also: #GtkGLArea, #GstBuffer
|
||||
*
|
||||
* #GtkGstGLWidget is an #GtkWidget that renders GStreamer video buffers.
|
||||
*/
|
||||
|
||||
#define GST_CAT_DEFAULT gtk_gst_gl_widget_debug
|
||||
GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
|
||||
|
||||
struct _GtkGstGLWidgetPrivate
|
||||
{
|
||||
gboolean initiated;
|
||||
GstGLDisplay *display;
|
||||
GdkGLContext *gdk_context;
|
||||
GstGLContext *other_context;
|
||||
GstGLContext *context;
|
||||
GstGLUpload *upload;
|
||||
GstGLShader *shader;
|
||||
GLuint vao;
|
||||
GLuint vertex_buffer;
|
||||
GLint attr_position;
|
||||
GLint attr_texture;
|
||||
GLuint current_tex;
|
||||
GstGLOverlayCompositor *overlay_compositor;
|
||||
};
|
||||
|
||||
static const GLfloat vertices[] = {
|
||||
1.0f, 1.0f, 0.0f, 1.0f, 0.0f,
|
||||
-1.0f, 1.0f, 0.0f, 0.0f, 0.0f,
|
||||
-1.0f, -1.0f, 0.0f, 0.0f, 1.0f,
|
||||
1.0f, -1.0f, 0.0f, 1.0f, 1.0f
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE_WITH_CODE (GtkGstGLWidget, gtk_gst_gl_widget, GTK_TYPE_GL_AREA,
|
||||
G_ADD_PRIVATE (GtkGstGLWidget)
|
||||
GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "gtkgstglwidget", 0,
|
||||
"GTK Gst GL Widget"));
|
||||
|
||||
static void
|
||||
gtk_gst_gl_widget_bind_buffer (GtkGstGLWidget * gst_widget)
|
||||
{
|
||||
GtkGstGLWidgetPrivate *priv = gst_widget->priv;
|
||||
const GstGLFuncs *gl = priv->context->gl_vtable;
|
||||
|
||||
gl->BindBuffer (GL_ARRAY_BUFFER, priv->vertex_buffer);
|
||||
|
||||
/* Load the vertex position */
|
||||
gl->VertexAttribPointer (priv->attr_position, 3, GL_FLOAT, GL_FALSE,
|
||||
5 * sizeof (GLfloat), (void *) 0);
|
||||
|
||||
/* Load the texture coordinate */
|
||||
gl->VertexAttribPointer (priv->attr_texture, 2, GL_FLOAT, GL_FALSE,
|
||||
5 * sizeof (GLfloat), (void *) (3 * sizeof (GLfloat)));
|
||||
|
||||
gl->EnableVertexAttribArray (priv->attr_position);
|
||||
gl->EnableVertexAttribArray (priv->attr_texture);
|
||||
}
|
||||
|
||||
static void
|
||||
gtk_gst_gl_widget_unbind_buffer (GtkGstGLWidget * gst_widget)
|
||||
{
|
||||
GtkGstGLWidgetPrivate *priv = gst_widget->priv;
|
||||
const GstGLFuncs *gl = priv->context->gl_vtable;
|
||||
|
||||
gl->BindBuffer (GL_ARRAY_BUFFER, 0);
|
||||
|
||||
gl->DisableVertexAttribArray (priv->attr_position);
|
||||
gl->DisableVertexAttribArray (priv->attr_texture);
|
||||
}
|
||||
|
||||
static void
|
||||
gtk_gst_gl_widget_init_redisplay (GtkGstGLWidget * gst_widget)
|
||||
{
|
||||
GtkGstGLWidgetPrivate *priv = gst_widget->priv;
|
||||
const GstGLFuncs *gl = priv->context->gl_vtable;
|
||||
GError *error = NULL;
|
||||
|
||||
gst_gl_insert_debug_marker (priv->other_context, "initializing redisplay");
|
||||
if (!(priv->shader = gst_gl_shader_new_default (priv->context, &error))) {
|
||||
GST_ERROR ("Failed to initialize shader: %s", error->message);
|
||||
return;
|
||||
}
|
||||
|
||||
priv->attr_position =
|
||||
gst_gl_shader_get_attribute_location (priv->shader, "a_position");
|
||||
priv->attr_texture =
|
||||
gst_gl_shader_get_attribute_location (priv->shader, "a_texcoord");
|
||||
|
||||
if (gl->GenVertexArrays) {
|
||||
gl->GenVertexArrays (1, &priv->vao);
|
||||
gl->BindVertexArray (priv->vao);
|
||||
}
|
||||
|
||||
gl->GenBuffers (1, &priv->vertex_buffer);
|
||||
gl->BindBuffer (GL_ARRAY_BUFFER, priv->vertex_buffer);
|
||||
gl->BufferData (GL_ARRAY_BUFFER, 4 * 5 * sizeof (GLfloat), vertices,
|
||||
GL_STATIC_DRAW);
|
||||
|
||||
if (gl->GenVertexArrays) {
|
||||
gtk_gst_gl_widget_bind_buffer (gst_widget);
|
||||
gl->BindVertexArray (0);
|
||||
}
|
||||
|
||||
gl->BindBuffer (GL_ARRAY_BUFFER, 0);
|
||||
|
||||
priv->overlay_compositor =
|
||||
gst_gl_overlay_compositor_new (priv->other_context);
|
||||
|
||||
priv->initiated = TRUE;
|
||||
}
|
||||
|
||||
static void
|
||||
_redraw_texture (GtkGstGLWidget * gst_widget, guint tex)
|
||||
{
|
||||
GtkGstGLWidgetPrivate *priv = gst_widget->priv;
|
||||
const GstGLFuncs *gl = priv->context->gl_vtable;
|
||||
const GLushort indices[] = { 0, 1, 2, 0, 2, 3 };
|
||||
|
||||
if (gst_widget->base.force_aspect_ratio) {
|
||||
GstVideoRectangle src, dst, result;
|
||||
gint widget_width, widget_height, widget_scale;
|
||||
|
||||
gl->ClearColor (0.0, 0.0, 0.0, 1.0);
|
||||
gl->Clear (GL_COLOR_BUFFER_BIT);
|
||||
|
||||
widget_scale = gtk_widget_get_scale_factor ((GtkWidget *) gst_widget);
|
||||
widget_width = gtk_widget_get_allocated_width ((GtkWidget *) gst_widget);
|
||||
widget_height = gtk_widget_get_allocated_height ((GtkWidget *) gst_widget);
|
||||
|
||||
src.x = 0;
|
||||
src.y = 0;
|
||||
src.w = gst_widget->base.display_width;
|
||||
src.h = gst_widget->base.display_height;
|
||||
|
||||
dst.x = 0;
|
||||
dst.y = 0;
|
||||
dst.w = widget_width * widget_scale;
|
||||
dst.h = widget_height * widget_scale;
|
||||
|
||||
gst_video_sink_center_rect (src, dst, &result, TRUE);
|
||||
|
||||
gl->Viewport (result.x, result.y, result.w, result.h);
|
||||
}
|
||||
|
||||
gst_gl_shader_use (priv->shader);
|
||||
|
||||
if (gl->BindVertexArray)
|
||||
gl->BindVertexArray (priv->vao);
|
||||
gtk_gst_gl_widget_bind_buffer (gst_widget);
|
||||
|
||||
gl->ActiveTexture (GL_TEXTURE0);
|
||||
gl->BindTexture (GL_TEXTURE_2D, tex);
|
||||
gst_gl_shader_set_uniform_1i (priv->shader, "tex", 0);
|
||||
|
||||
gl->DrawElements (GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indices);
|
||||
|
||||
if (gl->BindVertexArray)
|
||||
gl->BindVertexArray (0);
|
||||
else
|
||||
gtk_gst_gl_widget_unbind_buffer (gst_widget);
|
||||
|
||||
gl->BindTexture (GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
static inline void
|
||||
_draw_black (GstGLContext * context)
|
||||
{
|
||||
const GstGLFuncs *gl = context->gl_vtable;
|
||||
|
||||
gst_gl_insert_debug_marker (context, "rendering black");
|
||||
gl->ClearColor (0.0, 0.0, 0.0, 1.0);
|
||||
gl->Clear (GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
|
||||
static inline void
|
||||
_draw_black_with_gdk (GdkGLContext * gdk_context)
|
||||
{
|
||||
GST_DEBUG ("rendering empty frame with gdk context %p", gdk_context);
|
||||
glClearColor (0.0, 0.0, 0.0, 1.0);
|
||||
glClear (GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
|
||||
static gboolean
|
||||
gtk_gst_gl_widget_render (GtkGLArea * widget, GdkGLContext * context)
|
||||
{
|
||||
GtkGstGLWidgetPrivate *priv = GTK_GST_GL_WIDGET (widget)->priv;
|
||||
GtkGstBaseWidget *base_widget = GTK_GST_BASE_WIDGET (widget);
|
||||
|
||||
GTK_GST_BASE_WIDGET_LOCK (widget);
|
||||
|
||||
/* Draw black with GDK context when priv is not available yet.
|
||||
GTK calls render with GDK context already active. */
|
||||
if (!priv->context || !priv->other_context || base_widget->ignore_textures) {
|
||||
_draw_black_with_gdk (context);
|
||||
goto done;
|
||||
}
|
||||
|
||||
gst_gl_context_activate (priv->other_context, TRUE);
|
||||
|
||||
if (!priv->initiated || !base_widget->negotiated) {
|
||||
if (!priv->initiated)
|
||||
gtk_gst_gl_widget_init_redisplay (GTK_GST_GL_WIDGET (widget));
|
||||
|
||||
_draw_black (priv->other_context);
|
||||
goto done;
|
||||
}
|
||||
|
||||
/* Upload latest buffer */
|
||||
if (base_widget->pending_buffer) {
|
||||
GstBuffer *buffer = base_widget->pending_buffer;
|
||||
GstVideoFrame gl_frame;
|
||||
GstGLSyncMeta *sync_meta;
|
||||
|
||||
if (!gst_video_frame_map (&gl_frame, &base_widget->v_info, buffer,
|
||||
GST_MAP_READ | GST_MAP_GL)) {
|
||||
_draw_black (priv->other_context);
|
||||
goto done;
|
||||
}
|
||||
|
||||
priv->current_tex = *(guint *) gl_frame.data[0];
|
||||
gst_gl_insert_debug_marker (priv->other_context, "redrawing texture %u",
|
||||
priv->current_tex);
|
||||
|
||||
gst_gl_overlay_compositor_upload_overlays (priv->overlay_compositor,
|
||||
buffer);
|
||||
|
||||
sync_meta = gst_buffer_get_gl_sync_meta (buffer);
|
||||
if (sync_meta) {
|
||||
/* XXX: the set_sync() seems to be needed for resizing */
|
||||
gst_gl_sync_meta_set_sync_point (sync_meta, priv->context);
|
||||
gst_gl_sync_meta_wait (sync_meta, priv->other_context);
|
||||
}
|
||||
|
||||
gst_video_frame_unmap (&gl_frame);
|
||||
|
||||
if (base_widget->buffer)
|
||||
gst_buffer_unref (base_widget->buffer);
|
||||
|
||||
/* Keep the buffer to ensure current_tex stay valid */
|
||||
base_widget->buffer = buffer;
|
||||
base_widget->pending_buffer = NULL;
|
||||
}
|
||||
|
||||
GST_DEBUG ("rendering buffer %p with gdk context %p",
|
||||
base_widget->buffer, context);
|
||||
|
||||
_redraw_texture (GTK_GST_GL_WIDGET (widget), priv->current_tex);
|
||||
gst_gl_overlay_compositor_draw_overlays (priv->overlay_compositor);
|
||||
|
||||
gst_gl_insert_debug_marker (priv->other_context, "texture %u redrawn",
|
||||
priv->current_tex);
|
||||
|
||||
done:
|
||||
if (priv->other_context)
|
||||
gst_gl_context_activate (priv->other_context, FALSE);
|
||||
|
||||
GTK_GST_BASE_WIDGET_UNLOCK (widget);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static void
|
||||
_reset_gl (GtkGstGLWidget * gst_widget)
|
||||
{
|
||||
GtkGstGLWidgetPrivate *priv = gst_widget->priv;
|
||||
const GstGLFuncs *gl = priv->other_context->gl_vtable;
|
||||
|
||||
if (!priv->gdk_context)
|
||||
priv->gdk_context = gtk_gl_area_get_context (GTK_GL_AREA (gst_widget));
|
||||
|
||||
if (priv->gdk_context == NULL)
|
||||
return;
|
||||
|
||||
gdk_gl_context_make_current (priv->gdk_context);
|
||||
gst_gl_context_activate (priv->other_context, TRUE);
|
||||
|
||||
if (priv->vao) {
|
||||
gl->DeleteVertexArrays (1, &priv->vao);
|
||||
priv->vao = 0;
|
||||
}
|
||||
|
||||
if (priv->vertex_buffer) {
|
||||
gl->DeleteBuffers (1, &priv->vertex_buffer);
|
||||
priv->vertex_buffer = 0;
|
||||
}
|
||||
|
||||
if (priv->upload) {
|
||||
gst_object_unref (priv->upload);
|
||||
priv->upload = NULL;
|
||||
}
|
||||
|
||||
if (priv->shader) {
|
||||
gst_object_unref (priv->shader);
|
||||
priv->shader = NULL;
|
||||
}
|
||||
|
||||
if (priv->overlay_compositor)
|
||||
gst_object_unref (priv->overlay_compositor);
|
||||
|
||||
gst_gl_context_activate (priv->other_context, FALSE);
|
||||
|
||||
gst_object_unref (priv->other_context);
|
||||
priv->other_context = NULL;
|
||||
|
||||
gdk_gl_context_clear_current ();
|
||||
|
||||
g_object_unref (priv->gdk_context);
|
||||
priv->gdk_context = NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
gtk_gst_gl_widget_finalize (GObject * object)
|
||||
{
|
||||
GtkGstGLWidgetPrivate *priv = GTK_GST_GL_WIDGET (object)->priv;
|
||||
GtkGstBaseWidget *base_widget = GTK_GST_BASE_WIDGET (object);
|
||||
|
||||
if (priv->other_context)
|
||||
gst_gtk_invoke_on_main ((GThreadFunc) (GCallback) _reset_gl, base_widget);
|
||||
|
||||
if (priv->context)
|
||||
gst_object_unref (priv->context);
|
||||
|
||||
if (priv->display)
|
||||
gst_object_unref (priv->display);
|
||||
|
||||
gtk_gst_base_widget_finalize (object);
|
||||
G_OBJECT_CLASS (gtk_gst_gl_widget_parent_class)->finalize (object);
|
||||
}
|
||||
|
||||
static void
|
||||
gtk_gst_gl_widget_class_init (GtkGstGLWidgetClass * klass)
|
||||
{
|
||||
GObjectClass *gobject_klass = (GObjectClass *) klass;
|
||||
GtkGLAreaClass *gl_widget_klass = (GtkGLAreaClass *) klass;
|
||||
|
||||
gtk_gst_base_widget_class_init (GTK_GST_BASE_WIDGET_CLASS (klass));
|
||||
|
||||
gobject_klass->finalize = gtk_gst_gl_widget_finalize;
|
||||
gl_widget_klass->render = gtk_gst_gl_widget_render;
|
||||
}
|
||||
|
||||
static void
|
||||
gtk_gst_gl_widget_init (GtkGstGLWidget * gst_widget)
|
||||
{
|
||||
GtkGstBaseWidget *base_widget = GTK_GST_BASE_WIDGET (gst_widget);
|
||||
GdkDisplay *display;
|
||||
GtkGstGLWidgetPrivate *priv;
|
||||
|
||||
gtk_gst_base_widget_init (base_widget);
|
||||
|
||||
gst_widget->priv = priv = gtk_gst_gl_widget_get_instance_private (gst_widget);
|
||||
|
||||
display = gdk_display_get_default ();
|
||||
|
||||
#if GST_GL_HAVE_WINDOW_X11 && defined (GDK_WINDOWING_X11)
|
||||
if (GDK_IS_X11_DISPLAY (display)) {
|
||||
priv->display = (GstGLDisplay *)
|
||||
gst_gl_display_x11_new_with_display (gdk_x11_display_get_xdisplay
|
||||
(display));
|
||||
}
|
||||
#endif
|
||||
#if GST_GL_HAVE_WINDOW_WAYLAND && defined (GDK_WINDOWING_WAYLAND)
|
||||
if (GDK_IS_WAYLAND_DISPLAY (display)) {
|
||||
struct wl_display *wayland_display =
|
||||
gdk_wayland_display_get_wl_display (display);
|
||||
priv->display = (GstGLDisplay *)
|
||||
gst_gl_display_wayland_new_with_display (wayland_display);
|
||||
}
|
||||
#endif
|
||||
|
||||
(void) display;
|
||||
|
||||
if (!priv->display)
|
||||
priv->display = gst_gl_display_new ();
|
||||
|
||||
GST_INFO ("Created %" GST_PTR_FORMAT, priv->display);
|
||||
|
||||
/* GTK4 always has alpha */
|
||||
#if !defined(BUILD_FOR_GTK4)
|
||||
gtk_gl_area_set_has_alpha (GTK_GL_AREA (gst_widget),
|
||||
!base_widget->ignore_alpha);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void
|
||||
_get_gl_context (GtkGstGLWidget * gst_widget)
|
||||
{
|
||||
GtkGstGLWidgetPrivate *priv = gst_widget->priv;
|
||||
GstGLPlatform platform = GST_GL_PLATFORM_NONE;
|
||||
GstGLAPI gl_api = GST_GL_API_NONE;
|
||||
guintptr gl_handle = 0;
|
||||
|
||||
gtk_widget_realize (GTK_WIDGET (gst_widget));
|
||||
|
||||
if (priv->other_context)
|
||||
gst_object_unref (priv->other_context);
|
||||
priv->other_context = NULL;
|
||||
|
||||
if (priv->gdk_context)
|
||||
g_object_unref (priv->gdk_context);
|
||||
|
||||
priv->gdk_context = gtk_gl_area_get_context (GTK_GL_AREA (gst_widget));
|
||||
if (priv->gdk_context == NULL) {
|
||||
GError *error = gtk_gl_area_get_error (GTK_GL_AREA (gst_widget));
|
||||
|
||||
GST_ERROR_OBJECT (gst_widget, "Error creating GdkGLContext : %s",
|
||||
error ? error->message : "No error set by Gdk");
|
||||
g_clear_error (&error);
|
||||
return;
|
||||
}
|
||||
|
||||
g_object_ref (priv->gdk_context);
|
||||
|
||||
gdk_gl_context_make_current (priv->gdk_context);
|
||||
|
||||
#if GST_GL_HAVE_WINDOW_X11 && defined (GDK_WINDOWING_X11)
|
||||
if (GST_IS_GL_DISPLAY_X11 (priv->display)) {
|
||||
#if GST_GL_HAVE_PLATFORM_GLX
|
||||
if (!gl_handle) {
|
||||
platform = GST_GL_PLATFORM_GLX;
|
||||
gl_handle = gst_gl_context_get_current_gl_context (platform);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if GST_GL_HAVE_PLATFORM_EGL
|
||||
if (!gl_handle) {
|
||||
platform = GST_GL_PLATFORM_EGL;
|
||||
gl_handle = gst_gl_context_get_current_gl_context (platform);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (gl_handle) {
|
||||
gl_api = gst_gl_context_get_current_gl_api (platform, NULL, NULL);
|
||||
priv->other_context =
|
||||
gst_gl_context_new_wrapped (priv->display, gl_handle,
|
||||
platform, gl_api);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if GST_GL_HAVE_WINDOW_WAYLAND && GST_GL_HAVE_PLATFORM_EGL && defined (GDK_WINDOWING_WAYLAND)
|
||||
if (GST_IS_GL_DISPLAY_WAYLAND (priv->display)) {
|
||||
platform = GST_GL_PLATFORM_EGL;
|
||||
gl_api = gst_gl_context_get_current_gl_api (platform, NULL, NULL);
|
||||
gl_handle = gst_gl_context_get_current_gl_context (platform);
|
||||
if (gl_handle)
|
||||
priv->other_context =
|
||||
gst_gl_context_new_wrapped (priv->display, gl_handle,
|
||||
platform, gl_api);
|
||||
}
|
||||
#endif
|
||||
|
||||
(void) platform;
|
||||
(void) gl_api;
|
||||
(void) gl_handle;
|
||||
|
||||
if (priv->other_context) {
|
||||
GError *error = NULL;
|
||||
|
||||
GST_INFO ("Retrieved Gdk OpenGL context %" GST_PTR_FORMAT,
|
||||
priv->other_context);
|
||||
gst_gl_context_activate (priv->other_context, TRUE);
|
||||
if (!gst_gl_context_fill_info (priv->other_context, &error)) {
|
||||
GST_ERROR ("failed to retrieve gdk context info: %s", error->message);
|
||||
g_clear_error (&error);
|
||||
g_object_unref (priv->other_context);
|
||||
priv->other_context = NULL;
|
||||
} else {
|
||||
gst_gl_context_activate (priv->other_context, FALSE);
|
||||
}
|
||||
} else {
|
||||
GST_WARNING ("Could not retrieve Gdk OpenGL context");
|
||||
}
|
||||
}
|
||||
|
||||
GtkWidget *
|
||||
gtk_gst_gl_widget_new (void)
|
||||
{
|
||||
return (GtkWidget *) g_object_new (GTK_TYPE_GST_GL_WIDGET, NULL);
|
||||
}
|
||||
|
||||
gboolean
|
||||
gtk_gst_gl_widget_init_winsys (GtkGstGLWidget * gst_widget)
|
||||
{
|
||||
GtkGstGLWidgetPrivate *priv = gst_widget->priv;
|
||||
GError *error = NULL;
|
||||
|
||||
g_return_val_if_fail (GTK_IS_GST_GL_WIDGET (gst_widget), FALSE);
|
||||
g_return_val_if_fail (priv->display != NULL, FALSE);
|
||||
|
||||
GTK_GST_BASE_WIDGET_LOCK (gst_widget);
|
||||
|
||||
if (priv->display && priv->gdk_context && priv->other_context) {
|
||||
GST_TRACE ("have already initialized contexts");
|
||||
GTK_GST_BASE_WIDGET_UNLOCK (gst_widget);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (!priv->other_context) {
|
||||
GTK_GST_BASE_WIDGET_UNLOCK (gst_widget);
|
||||
gst_gtk_invoke_on_main ((GThreadFunc) (GCallback) _get_gl_context, gst_widget);
|
||||
GTK_GST_BASE_WIDGET_LOCK (gst_widget);
|
||||
}
|
||||
|
||||
if (!GST_IS_GL_CONTEXT (priv->other_context)) {
|
||||
GST_FIXME ("Could not retrieve Gdk OpenGL context");
|
||||
GTK_GST_BASE_WIDGET_UNLOCK (gst_widget);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
GST_OBJECT_LOCK (priv->display);
|
||||
if (!gst_gl_display_create_context (priv->display, priv->other_context,
|
||||
&priv->context, &error)) {
|
||||
GST_WARNING ("Could not create OpenGL context: %s",
|
||||
error ? error->message : "Unknown");
|
||||
g_clear_error (&error);
|
||||
GST_OBJECT_UNLOCK (priv->display);
|
||||
GTK_GST_BASE_WIDGET_UNLOCK (gst_widget);
|
||||
return FALSE;
|
||||
}
|
||||
gst_gl_display_add_context (priv->display, priv->context);
|
||||
GST_OBJECT_UNLOCK (priv->display);
|
||||
|
||||
GTK_GST_BASE_WIDGET_UNLOCK (gst_widget);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
GstGLContext *
|
||||
gtk_gst_gl_widget_get_gtk_context (GtkGstGLWidget * gst_widget)
|
||||
{
|
||||
if (!gst_widget->priv->other_context)
|
||||
return NULL;
|
||||
|
||||
return gst_object_ref (gst_widget->priv->other_context);
|
||||
}
|
||||
|
||||
GstGLContext *
|
||||
gtk_gst_gl_widget_get_context (GtkGstGLWidget * gst_widget)
|
||||
{
|
||||
if (!gst_widget->priv->context)
|
||||
return NULL;
|
||||
|
||||
return gst_object_ref (gst_widget->priv->context);
|
||||
}
|
||||
|
||||
GstGLDisplay *
|
||||
gtk_gst_gl_widget_get_display (GtkGstGLWidget * gst_widget)
|
||||
{
|
||||
if (!gst_widget->priv->display)
|
||||
return NULL;
|
||||
|
||||
return gst_object_ref (gst_widget->priv->display);
|
||||
}
|
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* GStreamer
|
||||
* Copyright (C) 2015 Matthew Waters <matthew@centricular.com>
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Library General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 2 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
|
||||
* Library General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Library General Public
|
||||
* License along with this library; if not, write to the
|
||||
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __GTK_GST_GL_WIDGET_H__
|
||||
#define __GTK_GST_GL_WIDGET_H__
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
#include <gst/gst.h>
|
||||
#include <gst/gl/gl.h>
|
||||
|
||||
#include "gtkgstbasewidget.h"
|
||||
|
||||
G_BEGIN_DECLS
|
||||
|
||||
GType gtk_gst_gl_widget_get_type (void);
|
||||
#define GTK_TYPE_GST_GL_WIDGET (gtk_gst_gl_widget_get_type())
|
||||
#define GTK_GST_GL_WIDGET(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),GTK_TYPE_GST_GL_WIDGET,GtkGstGLWidget))
|
||||
#define GTK_GST_GL_WIDGET_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass),GTK_TYPE_GST_GL_WIDGET,GtkGstGLWidgetClass))
|
||||
#define GTK_IS_GST_GL_WIDGET(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj),GTK_TYPE_GST_GL_WIDGET))
|
||||
#define GTK_IS_GST_GL_WIDGET_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass),GTK_TYPE_GST_GL_WIDGET))
|
||||
#define GTK_GST_GL_WIDGET_CAST(obj) ((GtkGstGLWidget*)(obj))
|
||||
|
||||
typedef struct _GtkGstGLWidget GtkGstGLWidget;
|
||||
typedef struct _GtkGstGLWidgetClass GtkGstGLWidgetClass;
|
||||
typedef struct _GtkGstGLWidgetPrivate GtkGstGLWidgetPrivate;
|
||||
|
||||
/**
|
||||
* GtkGstGLWidget:
|
||||
*
|
||||
* Opaque #GtkGstGLWidget object
|
||||
*/
|
||||
struct _GtkGstGLWidget
|
||||
{
|
||||
/* <private> */
|
||||
GtkGstBaseWidget base;
|
||||
|
||||
GtkGstGLWidgetPrivate *priv;
|
||||
};
|
||||
|
||||
/**
|
||||
* GtkGstGLWidgetClass:
|
||||
*
|
||||
* The #GtkGstGLWidgetClass struct only contains private data
|
||||
*/
|
||||
struct _GtkGstGLWidgetClass
|
||||
{
|
||||
/* <private> */
|
||||
GtkGstBaseWidgetClass base_class;
|
||||
};
|
||||
|
||||
GtkWidget * gtk_gst_gl_widget_new (void);
|
||||
|
||||
gboolean gtk_gst_gl_widget_init_winsys (GtkGstGLWidget * widget);
|
||||
GstGLDisplay * gtk_gst_gl_widget_get_display (GtkGstGLWidget * widget);
|
||||
GstGLContext * gtk_gst_gl_widget_get_context (GtkGstGLWidget * widget);
|
||||
GstGLContext * gtk_gst_gl_widget_get_gtk_context (GtkGstGLWidget * widget);
|
||||
|
||||
G_END_DECLS
|
||||
|
||||
#endif /* __GTK_GST_GL_WIDGET_H__ */
|
7
lib/gst/clapper/meson.build
vendored
7
lib/gst/clapper/meson.build
vendored
@@ -8,11 +8,9 @@ gstclapper_sources = [
|
||||
'gstclapper-visualization.c',
|
||||
'gstclapper-gtk4-plugin.c',
|
||||
|
||||
'gtk4/gstgtkbasesink.c',
|
||||
'gtk4/gstclapperglsink.c',
|
||||
'gtk4/gstgtkutils.c',
|
||||
'gtk4/gtkgstbasewidget.c',
|
||||
'gtk4/gstgtkglsink.c',
|
||||
'gtk4/gtkgstglwidget.c',
|
||||
'gtk4/gtkclapperglwidget.c',
|
||||
]
|
||||
gstclapper_headers = [
|
||||
'clapper.h',
|
||||
@@ -32,7 +30,6 @@ gstclapper_defines = [
|
||||
'-DBUILDING_GST_CLAPPER',
|
||||
'-DGST_USE_UNSTABLE_API',
|
||||
'-DHAVE_GTK_GL',
|
||||
'-DBUILD_FOR_GTK4',
|
||||
]
|
||||
gtk_deps = [gstgl_dep, gstglproto_dep]
|
||||
have_gtk_gl_windowing = false
|
||||
|
@@ -1,5 +1,5 @@
|
||||
project('com.github.rafostar.Clapper', 'c', 'cpp',
|
||||
version: '0.1.0',
|
||||
version: '0.2.0',
|
||||
meson_version: '>= 0.50.0',
|
||||
license: 'GPL3',
|
||||
default_options: [
|
||||
|
@@ -2,7 +2,7 @@ Format: 3.0 (quilt)
|
||||
Source: clapper
|
||||
Binary: clapper
|
||||
Architecture: any
|
||||
Version: 0.1.0
|
||||
Version: 0.2.0
|
||||
Maintainer: Rafostar <rafostar.github@gmail.com>
|
||||
Build-Depends: debhelper (>= 10),
|
||||
meson (>= 0.50),
|
||||
|
@@ -1,5 +1,5 @@
|
||||
clapper (0.1.0) unstable; urgency=low
|
||||
clapper (0.2.0) unstable; urgency=low
|
||||
|
||||
* New version
|
||||
|
||||
-- Rafostar <rafostar.github@gmail.com> Fri, 26 Feb 2021 09:39:00 +0100
|
||||
-- Rafostar <rafostar.github@gmail.com> Tue, 13 Apr 2021 09:39:00 +0100
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"app-id": "com.github.rafostar.Clapper",
|
||||
"runtime": "org.gnome.Platform",
|
||||
"runtime-version": "3.38",
|
||||
"runtime-version": "40",
|
||||
"sdk": "org.gnome.Sdk",
|
||||
"command": "com.github.rafostar.Clapper",
|
||||
"finish-args": [
|
||||
@@ -18,7 +18,6 @@
|
||||
"--env=GST_VAAPI_ALL_DRIVERS=1"
|
||||
],
|
||||
"modules": [
|
||||
"lib/glib-networking.json",
|
||||
"shared-modules/gudev/gudev.json",
|
||||
"lib/pango.json",
|
||||
"lib/libsass.json",
|
||||
|
@@ -0,0 +1,86 @@
|
||||
From be0f4bc94fad9fe182c97eef389954b5f63f7092 Mon Sep 17 00:00:00 2001
|
||||
From: Jun Xie <jun.xie@samsung.com>
|
||||
Date: Sat, 4 Nov 2017 14:48:54 +0800
|
||||
Subject: [PATCH] dashdemux: fix segmentBase type with 'sidx' not using range
|
||||
download issue
|
||||
|
||||
1. for utilizing range download and enable bitrate switch
|
||||
* update fragment info after 'sidx' is downloaded and parsed,
|
||||
so that media segment's range is set by 'sidx' entry info.
|
||||
* while updating fragment info, setting range_end by 'sidx' entry size.
|
||||
|
||||
2. for singleSegmentBase type WITHOUT @indexRange explicitly presented in MPD file
|
||||
* set '*sidx_seek_needed' to true, early terminate currently no-range downloading whole file,
|
||||
then jump to the requested SIDX entry by using sidx info.
|
||||
|
||||
3. for 'ref type 1' 'sidx'
|
||||
* keep current behaviour for 'ref type 1', download as a whole file without range download
|
||||
|
||||
https://bugzilla.gnome.org/show_bug.cgi?id=788763
|
||||
|
||||
diff --git a/ext/dash/gstdashdemux.c b/ext/dash/gstdashdemux.c
|
||||
index e38240800..7554a44b2 100644
|
||||
--- a/ext/dash/gstdashdemux.c
|
||||
+++ b/ext/dash/gstdashdemux.c
|
||||
@@ -1356,7 +1356,7 @@ gst_dash_demux_stream_update_fragment_info (GstAdaptiveDemuxStream * stream)
|
||||
stream->fragment.range_start + entry->size - 1;
|
||||
dashstream->actual_position += entry->duration;
|
||||
} else {
|
||||
- stream->fragment.range_end = fragment.range_end;
|
||||
+ stream->fragment.range_end = stream->fragment.range_start + entry->size - 1;
|
||||
}
|
||||
} else {
|
||||
dashstream->actual_position = stream->fragment.timestamp =
|
||||
@@ -1572,7 +1572,7 @@ gst_dash_demux_stream_has_next_subfragment (GstAdaptiveDemuxStream * stream)
|
||||
|
||||
if (dashstream->sidx_parser.status == GST_ISOFF_SIDX_PARSER_FINISHED) {
|
||||
if (stream->demux->segment.rate > 0.0) {
|
||||
- if (sidx->entry_index + 1 < sidx->entries_count)
|
||||
+ if (sidx->entry_index < sidx->entries_count)
|
||||
return TRUE;
|
||||
} else {
|
||||
if (sidx->entry_index >= 1)
|
||||
@@ -2903,6 +2903,7 @@ gst_dash_demux_parse_isobmff (GstAdaptiveDemux * demux,
|
||||
GstByteReader sub_reader;
|
||||
GstIsoffParserResult res;
|
||||
guint dummy;
|
||||
+ gboolean ref_type1_found = FALSE;
|
||||
|
||||
dash_stream->sidx_base_offset =
|
||||
dash_stream->isobmff_parser.current_start_offset + size;
|
||||
@@ -2932,6 +2933,7 @@ gst_dash_demux_parse_isobmff (GstAdaptiveDemux * demux,
|
||||
GST_FIXME_OBJECT (stream->pad, "SIDX ref_type 1 not supported yet");
|
||||
dash_stream->sidx_position = GST_CLOCK_TIME_NONE;
|
||||
gst_isoff_sidx_parser_clear (&dash_stream->sidx_parser);
|
||||
+ ref_type1_found = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -2968,8 +2970,9 @@ gst_dash_demux_parse_isobmff (GstAdaptiveDemux * demux,
|
||||
}
|
||||
}
|
||||
|
||||
- if (dash_stream->sidx_parser.status == GST_ISOFF_SIDX_PARSER_FINISHED &&
|
||||
- SIDX (dash_stream)->entry_index != 0) {
|
||||
+ if ((dash_stream->sidx_parser.status == GST_ISOFF_SIDX_PARSER_FINISHED &&
|
||||
+ SIDX (dash_stream)->entry_index != 0) || (!stream->downloading_index &&
|
||||
+ !ref_type1_found)) {
|
||||
/* Need to jump to the requested SIDX entry. Push everything up to
|
||||
* the SIDX box below and let the caller handle everything else */
|
||||
*sidx_seek_needed = TRUE;
|
||||
diff --git a/gst-libs/gst/adaptivedemux/gstadaptivedemux.c b/gst-libs/gst/adaptivedemux/gstadaptivedemux.c
|
||||
index a495ec2e7..3a09a76b1 100644
|
||||
--- a/gst-libs/gst/adaptivedemux/gstadaptivedemux.c
|
||||
+++ b/gst-libs/gst/adaptivedemux/gstadaptivedemux.c
|
||||
@@ -3378,6 +3378,9 @@ gst_adaptive_demux_stream_download_header_fragment (GstAdaptiveDemuxStream *
|
||||
ret = gst_adaptive_demux_stream_download_uri (demux, stream,
|
||||
stream->fragment.index_uri, stream->fragment.index_range_start,
|
||||
stream->fragment.index_range_end, NULL);
|
||||
+
|
||||
+ gst_adaptive_demux_stream_update_fragment_info(stream->demux, stream);
|
||||
+
|
||||
stream->downloading_index = FALSE;
|
||||
}
|
||||
}
|
||||
--
|
||||
2.7.4
|
@@ -36,6 +36,10 @@
|
||||
{
|
||||
"type": "patch",
|
||||
"path": "gst-plugins-bad-assrender-fix-mimetype-detection.patch"
|
||||
},
|
||||
{
|
||||
"type": "patch",
|
||||
"path": "gst-plugins-bad-dashdemux-sdix-range-download.patch"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -0,0 +1,34 @@
|
||||
From d42546dda8fdb3d044e715d0a6a1a74cd411acbe Mon Sep 17 00:00:00 2001
|
||||
From: Rafostar <40623528+Rafostar@users.noreply.github.com>
|
||||
Date: Mon, 5 Apr 2021 18:05:38 +0200
|
||||
Subject: [PATCH] GL: Do not set backbuffer on Wayland memory copy
|
||||
|
||||
This aims to workaround a Mesa bug that causes crash on Intel GPUs
|
||||
caused by calling "glDrawBuffer (GL_BACK)" on Wayland where
|
||||
there is no actual backbuffer in GStreamer OpenGL context.
|
||||
---
|
||||
gst-libs/gst/gl/gstglmemory.c | 8 +++++++-
|
||||
1 file changed, 7 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/gst-libs/gst/gl/gstglmemory.c b/gst-libs/gst/gl/gstglmemory.c
|
||||
index 76c04eb1b..cd3481847 100644
|
||||
--- a/gst-libs/gst/gl/gstglmemory.c
|
||||
+++ b/gst-libs/gst/gl/gstglmemory.c
|
||||
@@ -762,7 +762,13 @@ gst_gl_memory_copy_teximage (GstGLMemory * src, guint tex_id,
|
||||
gl->DeleteFramebuffers (n_fbos, &fbo[0]);
|
||||
|
||||
if (gl->DrawBuffer)
|
||||
- gl->DrawBuffer (GL_BACK);
|
||||
+ gl->DrawBuffer (
|
||||
+#if GST_GL_HAVE_WINDOW_WAYLAND
|
||||
+ GL_NONE
|
||||
+#else
|
||||
+ GL_BACK
|
||||
+#endif
|
||||
+ );
|
||||
}
|
||||
|
||||
gst_memory_unmap (GST_MEMORY_CAST (src), &sinfo);
|
||||
--
|
||||
2.28.0
|
||||
|
@@ -25,6 +25,10 @@
|
||||
{
|
||||
"type": "patch",
|
||||
"path": "gst-plugins-base-autodetect-subtitle-text-encoding.patch"
|
||||
},
|
||||
{
|
||||
"type": "patch",
|
||||
"path": "gst-plugins-base-do-not-set-backbuffer.patch"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -10,9 +10,10 @@
|
||||
"--disable-everything",
|
||||
"--enable-gpl",
|
||||
"--enable-version3",
|
||||
"--enable-shared",
|
||||
"--enable-optimizations",
|
||||
"--enable-runtime-cpudetect",
|
||||
"--enable-shared",
|
||||
"--enable-pthreads",
|
||||
"--enable-protocol=file",
|
||||
"--enable-decoder=flv,h263,h264,hevc,mjpeg,mpeg2video,mpeg4,mpegvideo,msmpeg4v1,msmpeg4v2,png,tiff,vc1,vp8,vp9,webp,wmv1,wmv2,wmv3,zerocodec",
|
||||
"--enable-decoder=aac,aac_fixed,aac_latm,ac3,ac3_fixed,eac3,flac,mp3,opus,tak,truehd,tta,wmalossless",
|
||||
@@ -22,8 +23,8 @@
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://git.ffmpeg.org/ffmpeg.git",
|
||||
"tag": "n4.3.1",
|
||||
"commit": "6b6b9e593dd4d3aaf75f48d40a13ef03bdef9fdb"
|
||||
"tag": "n4.4",
|
||||
"commit": "dc91b913b6260e85e1304c74ff7bb3c22a8c9fb1"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"name": "glib-networking",
|
||||
"buildsystem": "meson",
|
||||
"sources": [
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://gitlab.gnome.org/GNOME/glib-networking.git",
|
||||
"tag": "2.66.0",
|
||||
"commit": "61d7e024ca354e6d2e39930d66a2067f3de5842c"
|
||||
}
|
||||
]
|
||||
}
|
@@ -1,26 +0,0 @@
|
||||
From c6320cfd75c65bfb1736b7ca5afc9c0f5ffc09d7 Mon Sep 17 00:00:00 2001
|
||||
From: =?UTF-8?q?Rafa=C5=82=20Dzi=C4=99giel?= <rafostar.github@gmail.com>
|
||||
Date: Thu, 25 Feb 2021 09:45:38 +0100
|
||||
Subject: [PATCH] Broadway: fix unsafe variable type
|
||||
|
||||
Only guint32 guarantees to be always 32bit on all platforms. Mixing 32bit and 64bit memory sizes leads to a crash.
|
||||
---
|
||||
gdk/broadway/gdkbroadway-server.c | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
diff --git a/gdk/broadway/gdkbroadway-server.c b/gdk/broadway/gdkbroadway-server.c
|
||||
index 02b6f93183..e6b96ff0b9 100644
|
||||
--- a/gdk/broadway/gdkbroadway-server.c
|
||||
+++ b/gdk/broadway/gdkbroadway-server.c
|
||||
@@ -235,7 +235,7 @@ static void
|
||||
parse_all_input (GdkBroadwayServer *server)
|
||||
{
|
||||
guint8 *p, *end;
|
||||
- size_t size;
|
||||
+ guint32 size;
|
||||
BroadwayReply *reply;
|
||||
|
||||
p = server->recv_buffer;
|
||||
--
|
||||
2.26.2
|
||||
|
@@ -18,16 +18,11 @@
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://gitlab.gnome.org/GNOME/gtk.git",
|
||||
"tag": "4.1.1",
|
||||
"commit": "1f284fcd706de5b0b8c54fee3ff61880caf1d167"
|
||||
"commit": "5710df685b0af9b7dd306dfba6c7e174e428950e"
|
||||
},
|
||||
{
|
||||
"type": "patch",
|
||||
"path": "gtk4-popover-unrealize.patch"
|
||||
},
|
||||
{
|
||||
"type": "patch",
|
||||
"path": "gtk4-broadway-fix-unsafe-variable-type.patch"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@@ -26,7 +26,7 @@
|
||||
%global glib2_version 2.56.0
|
||||
|
||||
Name: clapper
|
||||
Version: 0.1.0
|
||||
Version: 0.2.0
|
||||
Release: 1%{?dist}
|
||||
Summary: Simple and modern GNOME media player
|
||||
|
||||
@@ -126,6 +126,9 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/*.desktop
|
||||
%{_libdir}/%{appname}/
|
||||
|
||||
%changelog
|
||||
* Tue Apr 13 2021 Rafostar <rafostar.github@gmail.com> - 0.2.0-1
|
||||
- New version
|
||||
|
||||
* Fri Feb 25 2021 Rafostar <rafostar.github@gmail.com> - 0.1.0-1
|
||||
- New version
|
||||
|
||||
|
37
src/app.js
37
src/app.js
@@ -12,10 +12,7 @@ class ClapperApp extends AppBase
|
||||
{
|
||||
super._init();
|
||||
|
||||
this.set_flags(
|
||||
this.get_flags()
|
||||
| Gio.ApplicationFlags.HANDLES_OPEN
|
||||
);
|
||||
this.flags |= Gio.ApplicationFlags.HANDLES_OPEN;
|
||||
}
|
||||
|
||||
vfunc_startup()
|
||||
@@ -23,45 +20,33 @@ class ClapperApp extends AppBase
|
||||
super.vfunc_startup();
|
||||
|
||||
const window = this.active_window;
|
||||
|
||||
window.isClapperApp = true;
|
||||
window.add_css_class('nobackground');
|
||||
|
||||
const clapperWidget = new Widget();
|
||||
window.set_child(clapperWidget);
|
||||
|
||||
const dummyHeaderbar = new Gtk.Box({
|
||||
can_focus: false,
|
||||
focusable: false,
|
||||
visible: false,
|
||||
});
|
||||
|
||||
window.add_css_class('nobackground');
|
||||
window.set_child(clapperWidget);
|
||||
window.set_titlebar(dummyHeaderbar);
|
||||
|
||||
this.mapSignal = window.connect('map', this._onWindowMap.bind(this));
|
||||
}
|
||||
|
||||
vfunc_open(files, hint)
|
||||
{
|
||||
super.vfunc_open(files, hint);
|
||||
|
||||
const { player } = this.active_window.get_child();
|
||||
|
||||
if(!this.doneFirstActivate)
|
||||
player._preparePlaylist(files);
|
||||
else
|
||||
player.set_playlist(files);
|
||||
|
||||
this._openFiles(files);
|
||||
this.activate();
|
||||
}
|
||||
|
||||
_onWindowShow(window)
|
||||
_onWindowMap(window)
|
||||
{
|
||||
super._onWindowShow(window);
|
||||
window.disconnect(this.mapSignal);
|
||||
this.mapSignal = null;
|
||||
|
||||
const { player } = this.active_window.get_child();
|
||||
const success = player.playlistWidget.nextTrack();
|
||||
|
||||
if(!success)
|
||||
debug('playlist is empty');
|
||||
|
||||
player.widget.grab_focus();
|
||||
window.child._onWindowMap(window);
|
||||
}
|
||||
});
|
||||
|
@@ -33,20 +33,17 @@ class ClapperAppBase extends Gtk.Application
|
||||
if(!settings.get_boolean('render-shadows'))
|
||||
window.add_css_class('gpufriendly');
|
||||
|
||||
if(
|
||||
settings.get_boolean('dark-theme')
|
||||
&& settings.get_boolean('brighter-sliders')
|
||||
)
|
||||
window.add_css_class('brightscale');
|
||||
|
||||
for(let action in Menu.actions) {
|
||||
const simpleAction = new Gio.SimpleAction({
|
||||
name: action
|
||||
});
|
||||
simpleAction.connect(
|
||||
'activate', () => Menu.actions[action](this.active_window)
|
||||
'activate', () => Menu.actions[action].run(this.active_window)
|
||||
);
|
||||
this.add_action(simpleAction);
|
||||
|
||||
if(Menu.actions[action].accels)
|
||||
this.set_accels_for_action(`app.${action}`, Menu.actions[action].accels);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +59,17 @@ class ClapperAppBase extends Gtk.Application
|
||||
);
|
||||
}
|
||||
|
||||
_openFiles(files)
|
||||
{
|
||||
const [playlist, subs] = Misc.parsePlaylistFiles(files);
|
||||
const { player } = this.active_window.get_child();
|
||||
|
||||
if(playlist && playlist.length)
|
||||
player.set_playlist(playlist);
|
||||
if(subs)
|
||||
player.set_subtitles(subs);
|
||||
}
|
||||
|
||||
_onFirstActivate()
|
||||
{
|
||||
const gtkSettings = Gtk.Settings.get_default();
|
||||
@@ -71,24 +79,17 @@ class ClapperAppBase extends Gtk.Application
|
||||
Gio.SettingsBindFlags.GET
|
||||
);
|
||||
this._onThemeChanged(gtkSettings);
|
||||
this._onIconThemeChanged(gtkSettings);
|
||||
gtkSettings.connect('notify::gtk-theme-name', this._onThemeChanged.bind(this));
|
||||
|
||||
this.windowShowSignal = this.active_window.connect(
|
||||
'show', this._onWindowShow.bind(this)
|
||||
);
|
||||
gtkSettings.connect('notify::gtk-icon-theme-name', this._onIconThemeChanged.bind(this));
|
||||
this.doneFirstActivate = true;
|
||||
}
|
||||
|
||||
_onWindowShow(window)
|
||||
{
|
||||
window.disconnect(this.windowShowSignal);
|
||||
this.windowShowSignal = null;
|
||||
}
|
||||
|
||||
_onThemeChanged(gtkSettings)
|
||||
{
|
||||
const theme = gtkSettings.gtk_theme_name;
|
||||
const window = this.active_window;
|
||||
const hasAdwThemeDark = window.has_css_class('adwthemedark');
|
||||
|
||||
debug(`user selected theme: ${theme}`);
|
||||
|
||||
@@ -97,6 +98,17 @@ class ClapperAppBase extends Gtk.Application
|
||||
if(!window.has_css_class('adwrounded'))
|
||||
window.add_css_class('adwrounded');
|
||||
|
||||
if(theme.startsWith('Adwaita') || theme.startsWith('Default')) {
|
||||
const isDarkTheme = settings.get_boolean('dark-theme');
|
||||
|
||||
if(isDarkTheme && !hasAdwThemeDark)
|
||||
window.add_css_class('adwthemedark');
|
||||
else if(!isDarkTheme && hasAdwThemeDark)
|
||||
window.remove_css_class('adwthemedark');
|
||||
}
|
||||
else if(hasAdwThemeDark)
|
||||
window.remove_css_class('adwthemedark');
|
||||
|
||||
if(!theme.endsWith('-dark'))
|
||||
return;
|
||||
|
||||
@@ -107,4 +119,18 @@ class ClapperAppBase extends Gtk.Application
|
||||
gtkSettings.gtk_theme_name = parsedTheme;
|
||||
debug(`set theme: ${parsedTheme}`);
|
||||
}
|
||||
|
||||
_onIconThemeChanged(gtkSettings)
|
||||
{
|
||||
const iconTheme = gtkSettings.gtk_icon_theme_name;
|
||||
const window = this.active_window;
|
||||
const hasAdwIcons = window.has_css_class('adwicons');
|
||||
|
||||
if(iconTheme === 'Adwaita' || iconTheme === 'Default') {
|
||||
if(!hasAdwIcons)
|
||||
window.add_css_class('adwicons');
|
||||
}
|
||||
else if(hasAdwIcons)
|
||||
window.remove_css_class('adwicons');
|
||||
}
|
||||
});
|
||||
|
151
src/assets/node-ytdl-core/sig.js
Normal file
151
src/assets/node-ytdl-core/sig.js
Normal file
@@ -0,0 +1,151 @@
|
||||
/* Copyright (C) 2012-present by fent
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
const jsVarStr = '[a-zA-Z_\\$][a-zA-Z_0-9]*';
|
||||
const jsSingleQuoteStr = `'[^'\\\\]*(:?\\\\[\\s\\S][^'\\\\]*)*'`;
|
||||
const jsDoubleQuoteStr = `"[^"\\\\]*(:?\\\\[\\s\\S][^"\\\\]*)*"`;
|
||||
const jsQuoteStr = `(?:${jsSingleQuoteStr}|${jsDoubleQuoteStr})`;
|
||||
const jsKeyStr = `(?:${jsVarStr}|${jsQuoteStr})`;
|
||||
const jsPropStr = `(?:\\.${jsVarStr}|\\[${jsQuoteStr}\\])`;
|
||||
const jsEmptyStr = `(?:''|"")`;
|
||||
const reverseStr = ':function\\(a\\)\\{' +
|
||||
'(?:return )?a\\.reverse\\(\\)' +
|
||||
'\\}';
|
||||
const sliceStr = ':function\\(a,b\\)\\{' +
|
||||
'return a\\.slice\\(b\\)' +
|
||||
'\\}';
|
||||
const spliceStr = ':function\\(a,b\\)\\{' +
|
||||
'a\\.splice\\(0,b\\)' +
|
||||
'\\}';
|
||||
const swapStr = ':function\\(a,b\\)\\{' +
|
||||
'var c=a\\[0\\];a\\[0\\]=a\\[b(?:%a\\.length)?\\];a\\[b(?:%a\\.length)?\\]=c(?:;return a)?' +
|
||||
'\\}';
|
||||
const actionsObjRegexp = new RegExp(
|
||||
`var (${jsVarStr})=\\{((?:(?:${
|
||||
jsKeyStr}${reverseStr}|${
|
||||
jsKeyStr}${sliceStr}|${
|
||||
jsKeyStr}${spliceStr}|${
|
||||
jsKeyStr}${swapStr
|
||||
}),?\\r?\\n?)+)\\};`);
|
||||
const actionsFuncRegexp = new RegExp(`${`function(?: ${jsVarStr})?\\(a\\)\\{` +
|
||||
`a=a\\.split\\(${jsEmptyStr}\\);\\s*` +
|
||||
`((?:(?:a=)?${jsVarStr}`}${
|
||||
jsPropStr
|
||||
}\\(a,\\d+\\);)+)` +
|
||||
`return a\\.join\\(${jsEmptyStr}\\)` +
|
||||
`\\}`);
|
||||
const reverseRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${reverseStr}`, 'm');
|
||||
const sliceRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${sliceStr}`, 'm');
|
||||
const spliceRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${spliceStr}`, 'm');
|
||||
const swapRegexp = new RegExp(`(?:^|,)(${jsKeyStr})${swapStr}`, 'm');
|
||||
|
||||
const swapHeadAndPosition = (arr, position) => {
|
||||
const first = arr[0];
|
||||
arr[0] = arr[position % arr.length];
|
||||
arr[position] = first;
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
function decipher(sig, tokens) {
|
||||
sig = sig.split('');
|
||||
tokens = tokens.split(',');
|
||||
|
||||
for(let i = 0, len = tokens.length; i < len; i++) {
|
||||
let token = tokens[i], pos;
|
||||
switch (token[0]) {
|
||||
case 'r':
|
||||
sig = sig.reverse();
|
||||
break;
|
||||
case 'w':
|
||||
pos = ~~token.slice(1);
|
||||
sig = swapHeadAndPosition(sig, pos);
|
||||
break;
|
||||
case 's':
|
||||
pos = ~~token.slice(1);
|
||||
sig = sig.slice(pos);
|
||||
break;
|
||||
case 'p':
|
||||
pos = ~~token.slice(1);
|
||||
sig.splice(0, pos);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return sig.join('');
|
||||
};
|
||||
|
||||
function extractActions(body) {
|
||||
const objResult = actionsObjRegexp.exec(body);
|
||||
const funcResult = actionsFuncRegexp.exec(body);
|
||||
|
||||
if(!objResult || !funcResult)
|
||||
return null;
|
||||
|
||||
const obj = objResult[1].replace(/\$/g, '\\$');
|
||||
const objBody = objResult[2].replace(/\$/g, '\\$');
|
||||
const funcBody = funcResult[1].replace(/\$/g, '\\$');
|
||||
|
||||
let result = reverseRegexp.exec(objBody);
|
||||
const reverseKey = result && result[1]
|
||||
.replace(/\$/g, '\\$')
|
||||
.replace(/\$|^'|^"|'$|"$/g, '');
|
||||
result = sliceRegexp.exec(objBody);
|
||||
const sliceKey = result && result[1]
|
||||
.replace(/\$/g, '\\$')
|
||||
.replace(/\$|^'|^"|'$|"$/g, '');
|
||||
result = spliceRegexp.exec(objBody);
|
||||
const spliceKey = result && result[1]
|
||||
.replace(/\$/g, '\\$')
|
||||
.replace(/\$|^'|^"|'$|"$/g, '');
|
||||
result = swapRegexp.exec(objBody);
|
||||
const swapKey = result && result[1]
|
||||
.replace(/\$/g, '\\$')
|
||||
.replace(/\$|^'|^"|'$|"$/g, '');
|
||||
|
||||
const keys = `(${[reverseKey, sliceKey, spliceKey, swapKey].join('|')})`;
|
||||
const myreg = `(?:a=)?${obj
|
||||
}(?:\\.${keys}|\\['${keys}'\\]|\\["${keys}"\\])` +
|
||||
`\\(a,(\\d+)\\)`;
|
||||
const tokenizeRegexp = new RegExp(myreg, 'g');
|
||||
const tokens = [];
|
||||
|
||||
while((result = tokenizeRegexp.exec(funcBody)) !== null) {
|
||||
const key = result[1] || result[2] || result[3];
|
||||
const pos = result[4];
|
||||
switch (key) {
|
||||
case swapKey:
|
||||
tokens.push(`w${result[4]}`);
|
||||
break;
|
||||
case reverseKey:
|
||||
tokens.push('r');
|
||||
break;
|
||||
case sliceKey:
|
||||
tokens.push(`s${result[4]}`);
|
||||
break;
|
||||
case spliceKey:
|
||||
tokens.push(`p${result[4]}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return tokens.join(',');
|
||||
}
|
@@ -1,5 +1,11 @@
|
||||
const { GObject, Gtk } = imports.gi;
|
||||
|
||||
/* Negative values from CSS */
|
||||
const PopoverOffset = {
|
||||
DEFAULT: -3,
|
||||
TVMODE: -5,
|
||||
};
|
||||
|
||||
var CustomButton = GObject.registerClass(
|
||||
class ClapperCustomButton extends Gtk.Button
|
||||
{
|
||||
@@ -8,10 +14,8 @@ class ClapperCustomButton extends Gtk.Button
|
||||
opts = opts || {};
|
||||
|
||||
const defaults = {
|
||||
margin_top: 4,
|
||||
margin_bottom: 4,
|
||||
margin_start: 2,
|
||||
margin_end: 2,
|
||||
halign: Gtk.Align.CENTER,
|
||||
valign: Gtk.Align.CENTER,
|
||||
can_focus: false,
|
||||
};
|
||||
Object.assign(opts, defaults);
|
||||
@@ -27,9 +31,6 @@ class ClapperCustomButton extends Gtk.Button
|
||||
if(this.isFullscreen === isFullscreen)
|
||||
return;
|
||||
|
||||
this.margin_top = (isFullscreen) ? 5 : 4;
|
||||
this.margin_start = (isFullscreen) ? 3 : 2;
|
||||
this.margin_end = (isFullscreen) ? 3 : 2;
|
||||
this.can_focus = isFullscreen;
|
||||
|
||||
/* Redraw icon after style class change */
|
||||
@@ -79,10 +80,8 @@ class ClapperPopoverButtonBase extends Gtk.ToggleButton
|
||||
_init()
|
||||
{
|
||||
super._init({
|
||||
margin_top: 4,
|
||||
margin_bottom: 4,
|
||||
margin_start: 2,
|
||||
margin_end: 2,
|
||||
halign: Gtk.Align.CENTER,
|
||||
valign: Gtk.Align.CENTER,
|
||||
can_focus: false,
|
||||
});
|
||||
|
||||
@@ -97,7 +96,7 @@ class ClapperPopoverButtonBase extends Gtk.ToggleButton
|
||||
});
|
||||
|
||||
this.popover.set_child(this.popoverBox);
|
||||
this.popover.set_offset(0, -this.margin_top);
|
||||
this.popover.set_offset(0, PopoverOffset.DEFAULT);
|
||||
|
||||
if(this.isFullscreen)
|
||||
this.popover.add_css_class('osd');
|
||||
@@ -111,9 +110,6 @@ class ClapperPopoverButtonBase extends Gtk.ToggleButton
|
||||
if(this.isFullscreen === isFullscreen)
|
||||
return;
|
||||
|
||||
this.margin_top = (isFullscreen) ? 5 : 4;
|
||||
this.margin_start = (isFullscreen) ? 3 : 2;
|
||||
this.margin_end = (isFullscreen) ? 3 : 2;
|
||||
this.can_focus = isFullscreen;
|
||||
|
||||
/* Redraw icon after style class change */
|
||||
@@ -122,7 +118,12 @@ class ClapperPopoverButtonBase extends Gtk.ToggleButton
|
||||
|
||||
this.isFullscreen = isFullscreen;
|
||||
|
||||
this.popover.set_offset(0, -this.margin_top);
|
||||
/* TODO: Fullscreen non-tv mode */
|
||||
const offset = (isFullscreen)
|
||||
? PopoverOffset.TVMODE
|
||||
: PopoverOffset.DEFAULT;
|
||||
|
||||
this.popover.set_offset(0, offset);
|
||||
|
||||
const cssClass = 'osd';
|
||||
if(isFullscreen === this.popover.has_css_class(cssClass))
|
||||
@@ -189,7 +190,7 @@ class ClapperLabelPopoverButton extends PopoverButtonBase
|
||||
label: text,
|
||||
single_line_mode: true,
|
||||
});
|
||||
this.customLabel.add_css_class('labelbutton');
|
||||
this.customLabel.add_css_class('labelbuttonlabel');
|
||||
this.set_child(this.customLabel);
|
||||
}
|
||||
|
||||
|
8
src/controls.js
vendored
8
src/controls.js
vendored
@@ -4,9 +4,6 @@ const Debug = imports.src.debug;
|
||||
const Misc = imports.src.misc;
|
||||
const Revealers = imports.src.revealers;
|
||||
|
||||
const CONTROLS_MARGIN = 2;
|
||||
const CONTROLS_SPACING = 0;
|
||||
|
||||
const { debug } = Debug;
|
||||
const { settings } = Misc;
|
||||
|
||||
@@ -17,9 +14,6 @@ class ClapperControls extends Gtk.Box
|
||||
{
|
||||
super._init({
|
||||
orientation: Gtk.Orientation.HORIZONTAL,
|
||||
margin_start: CONTROLS_MARGIN,
|
||||
margin_end: CONTROLS_MARGIN,
|
||||
spacing: CONTROLS_SPACING,
|
||||
valign: Gtk.Align.END,
|
||||
can_focus: false,
|
||||
});
|
||||
@@ -439,7 +433,7 @@ class ClapperControls extends Gtk.Box
|
||||
const scaleHeight = this.positionScale.parent.get_height();
|
||||
|
||||
this.chapterPopover.set_pointing_to(new Gdk.Rectangle({
|
||||
x: 2,
|
||||
x: -2,
|
||||
y: -(controlsHeight - scaleHeight) / 2,
|
||||
width: 2 * end,
|
||||
height: 0,
|
||||
|
165
src/dash.js
Normal file
165
src/dash.js
Normal file
@@ -0,0 +1,165 @@
|
||||
const Debug = imports.src.debug;
|
||||
const FileOps = imports.src.fileOps;
|
||||
const Misc = imports.src.misc;
|
||||
|
||||
const { debug } = Debug;
|
||||
|
||||
function generateDash(dashInfo)
|
||||
{
|
||||
debug('generating dash');
|
||||
|
||||
const bufferSec = Math.min(4, dashInfo.duration);
|
||||
|
||||
const dash = [
|
||||
`<?xml version="1.0" encoding="UTF-8"?>`,
|
||||
`<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`,
|
||||
` xmlns="urn:mpeg:dash:schema:mpd:2011"`,
|
||||
` xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd"`,
|
||||
` type="static"`,
|
||||
` mediaPresentationDuration="PT${dashInfo.duration}S"`,
|
||||
` minBufferTime="PT${bufferSec}S"`,
|
||||
` profiles="urn:mpeg:dash:profile:isoff-on-demand:2011">`,
|
||||
` <Period>`
|
||||
];
|
||||
|
||||
for(let adaptation of dashInfo.adaptations)
|
||||
dash.push(_addAdaptationSet(adaptation));
|
||||
|
||||
dash.push(
|
||||
` </Period>`,
|
||||
`</MPD>`
|
||||
);
|
||||
|
||||
debug('dash generated');
|
||||
|
||||
return dash.join('\n');
|
||||
}
|
||||
|
||||
function _addAdaptationSet(streamsArr)
|
||||
{
|
||||
/* We just need it for adaptation type,
|
||||
* so any stream will do */
|
||||
const { mimeInfo } = streamsArr[0];
|
||||
|
||||
const adaptArr = [
|
||||
`contentType="${mimeInfo.content}"`,
|
||||
`mimeType="${mimeInfo.type}"`,
|
||||
`subsegmentAlignment="true"`,
|
||||
`subsegmentStartsWithSAP="1"`,
|
||||
];
|
||||
|
||||
const widthArr = [];
|
||||
const heightArr = [];
|
||||
const fpsArr = [];
|
||||
|
||||
const representations = [];
|
||||
|
||||
for(let stream of streamsArr) {
|
||||
/* No point parsing if no URL */
|
||||
if(!stream.url)
|
||||
continue;
|
||||
|
||||
if(stream.width && stream.height) {
|
||||
widthArr.push(stream.width);
|
||||
heightArr.push(stream.height);
|
||||
}
|
||||
if(stream.fps)
|
||||
fpsArr.push(stream.fps);
|
||||
|
||||
representations.push(_getStreamRepresentation(stream));
|
||||
}
|
||||
|
||||
if(widthArr.length && heightArr.length) {
|
||||
const maxWidth = Math.max.apply(null, widthArr);
|
||||
const maxHeight = Math.max.apply(null, heightArr);
|
||||
const par = _getPar(maxWidth, maxHeight);
|
||||
|
||||
adaptArr.push(`maxWidth="${maxWidth}"`);
|
||||
adaptArr.push(`maxHeight="${maxHeight}"`);
|
||||
adaptArr.push(`par="${par}"`);
|
||||
}
|
||||
if(fpsArr.length) {
|
||||
const maxFps = Math.max.apply(null, fpsArr);
|
||||
|
||||
adaptArr.push(`maxFrameRate="${maxFps}"`);
|
||||
}
|
||||
|
||||
const adaptationSet = [
|
||||
` <AdaptationSet ${adaptArr.join(' ')}>`,
|
||||
representations.join('\n'),
|
||||
` </AdaptationSet>`
|
||||
];
|
||||
|
||||
return adaptationSet.join('\n');
|
||||
}
|
||||
|
||||
function _getStreamRepresentation(stream)
|
||||
{
|
||||
const repOptsArr = [
|
||||
`id="${stream.itag}"`,
|
||||
`codecs="${stream.mimeInfo.codecs}"`,
|
||||
`bandwidth="${stream.bitrate}"`,
|
||||
];
|
||||
|
||||
if(stream.width && stream.height) {
|
||||
repOptsArr.push(`width="${stream.width}"`);
|
||||
repOptsArr.push(`height="${stream.height}"`);
|
||||
repOptsArr.push(`sar="1:1"`);
|
||||
}
|
||||
if(stream.fps)
|
||||
repOptsArr.push(`frameRate="${stream.fps}"`);
|
||||
|
||||
const repArr = [
|
||||
` <Representation ${repOptsArr.join(' ')}>`,
|
||||
];
|
||||
if(stream.audioChannels) {
|
||||
const audioConfArr = [
|
||||
`schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011"`,
|
||||
`value="${stream.audioChannels}"`,
|
||||
];
|
||||
repArr.push(` <AudioChannelConfiguration ${audioConfArr.join(' ')}/>`);
|
||||
}
|
||||
|
||||
repArr.push(
|
||||
` <BaseURL>${stream.url}</BaseURL>`
|
||||
);
|
||||
|
||||
if(stream.indexRange) {
|
||||
const segRange = `${stream.indexRange.start}-${stream.indexRange.end}`;
|
||||
repArr.push(
|
||||
` <SegmentBase indexRange="${segRange}">`
|
||||
);
|
||||
if(stream.initRange) {
|
||||
const initRange = `${stream.initRange.start}-${stream.initRange.end}`;
|
||||
repArr.push(
|
||||
` <Initialization range="${initRange}"/>`
|
||||
);
|
||||
}
|
||||
repArr.push(
|
||||
` </SegmentBase>`
|
||||
);
|
||||
}
|
||||
|
||||
repArr.push(
|
||||
` </Representation>`
|
||||
);
|
||||
|
||||
return repArr.join('\n');
|
||||
}
|
||||
|
||||
function _getPar(width, height)
|
||||
{
|
||||
const gcd = _getGCD(width, height);
|
||||
|
||||
width /= gcd;
|
||||
height /= gcd;
|
||||
|
||||
return `${width}:${height}`;
|
||||
}
|
||||
|
||||
function _getGCD(width, height)
|
||||
{
|
||||
return (height)
|
||||
? _getGCD(height, width % height)
|
||||
: width;
|
||||
}
|
@@ -17,6 +17,7 @@ const ShellProxyWrapper = Gio.DBusProxy.makeProxyWrapper(`
|
||||
|
||||
let shellProxy = null;
|
||||
|
||||
debug('initializing GNOME Shell DBus proxy');
|
||||
new ShellProxyWrapper(
|
||||
Gio.DBus.session,
|
||||
'org.gnome.Shell',
|
||||
|
51
src/debug.js
51
src/debug.js
@@ -19,23 +19,46 @@ clapperDebugger.enabled = (
|
||||
|| G_DEBUG_ENV != null
|
||||
&& G_DEBUG_ENV.includes('Clapper')
|
||||
);
|
||||
const clapperDebug = clapperDebugger.debug;
|
||||
|
||||
function debug(msg, levelName)
|
||||
const ytDebugger = new Debug.Debugger('YouTube', {
|
||||
name_printer: new Ink.Printer({
|
||||
font: Ink.Font.BOLD,
|
||||
color: Ink.Color.RED
|
||||
}),
|
||||
time_printer: new Ink.Printer({
|
||||
color: Ink.Color.LIGHT_BLUE
|
||||
}),
|
||||
high_precision: true,
|
||||
});
|
||||
|
||||
function _debug(msg, debuggerName)
|
||||
{
|
||||
levelName = levelName || 'LEVEL_DEBUG';
|
||||
|
||||
if(msg.message) {
|
||||
levelName = 'LEVEL_CRITICAL';
|
||||
msg = msg.message;
|
||||
GLib.log_structured(
|
||||
debuggerName, GLib.LogLevelFlags.LEVEL_CRITICAL, {
|
||||
MESSAGE: msg.message,
|
||||
SYSLOG_IDENTIFIER: debuggerName.toLowerCase()
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if(levelName !== 'LEVEL_CRITICAL')
|
||||
return clapperDebug(msg);
|
||||
|
||||
GLib.log_structured(
|
||||
'Clapper', GLib.LogLevelFlags[levelName], {
|
||||
MESSAGE: msg,
|
||||
SYSLOG_IDENTIFIER: 'clapper'
|
||||
});
|
||||
switch(debuggerName) {
|
||||
case 'Clapper':
|
||||
clapperDebugger.debug(msg);
|
||||
break;
|
||||
case 'YouTube':
|
||||
ytDebugger.debug(msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function debug(msg)
|
||||
{
|
||||
_debug(msg, 'Clapper');
|
||||
}
|
||||
|
||||
function ytDebug(msg)
|
||||
{
|
||||
_debug(msg, 'YouTube');
|
||||
}
|
||||
|
@@ -24,10 +24,7 @@ class ClapperFileChooser extends Gtk.FileChooserNative
|
||||
filter.add_mime_type('video/*');
|
||||
filter.add_mime_type('audio/*');
|
||||
filter.add_mime_type('application/claps');
|
||||
this.subsMimes = [
|
||||
'application/x-subrip',
|
||||
];
|
||||
this.subsMimes.forEach(mime => filter.add_mime_type(mime));
|
||||
Misc.subsMimes.forEach(mime => filter.add_mime_type(mime));
|
||||
this.add_filter(filter);
|
||||
|
||||
this.responseSignal = this.connect('response', this._onResponse.bind(this));
|
||||
@@ -46,36 +43,26 @@ class ClapperFileChooser extends Gtk.FileChooserNative
|
||||
|
||||
if(response === Gtk.ResponseType.ACCEPT) {
|
||||
const files = this.get_files();
|
||||
const playlist = [];
|
||||
const filesArray = [];
|
||||
|
||||
let index = 0;
|
||||
let file;
|
||||
let subs;
|
||||
|
||||
while((file = files.get_item(index))) {
|
||||
const filename = file.get_basename();
|
||||
const [type, isUncertain] = Gio.content_type_guess(filename, null);
|
||||
|
||||
if(this.subsMimes.includes(type)) {
|
||||
subs = file;
|
||||
files.remove(index);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
playlist.push(file);
|
||||
filesArray.push(file);
|
||||
index++;
|
||||
}
|
||||
|
||||
const { player } = this.get_transient_for().get_child();
|
||||
const { application } = this.transient_for;
|
||||
const isHandlesOpen = Boolean(
|
||||
application.flags & Gio.ApplicationFlags.HANDLES_OPEN
|
||||
);
|
||||
|
||||
if(playlist.length)
|
||||
player.set_playlist(playlist);
|
||||
|
||||
/* add subs to single selected video
|
||||
or to already playing file */
|
||||
if(subs && !files.get_item(1))
|
||||
player.set_subtitles(subs);
|
||||
/* Remote app does not handle open */
|
||||
if(isHandlesOpen)
|
||||
application.open(filesArray, "");
|
||||
else
|
||||
application._openFiles(filesArray);
|
||||
}
|
||||
|
||||
this.unref();
|
||||
|
128
src/fileOps.js
Normal file
128
src/fileOps.js
Normal file
@@ -0,0 +1,128 @@
|
||||
const { Gio, GLib } = imports.gi;
|
||||
const ByteArray = imports.byteArray;
|
||||
const Debug = imports.src.debug;
|
||||
const Misc = imports.src.misc;
|
||||
|
||||
const { debug } = Debug;
|
||||
|
||||
/* FIXME: Use Gio._LocalFilePrototype once we are safe to assume
|
||||
* that GJS with https://gitlab.gnome.org/GNOME/gjs/-/commit/ec9385b8 is used. */
|
||||
const LocalFilePrototype = Gio.File.new_for_path('/').constructor.prototype;
|
||||
|
||||
Gio._promisify(LocalFilePrototype, 'load_bytes_async', 'load_bytes_finish');
|
||||
Gio._promisify(LocalFilePrototype, 'make_directory_async', 'make_directory_finish');
|
||||
Gio._promisify(LocalFilePrototype, 'replace_contents_bytes_async', 'replace_contents_finish');
|
||||
|
||||
function createCacheDirPromise()
|
||||
{
|
||||
const dir = Gio.File.new_for_path(
|
||||
GLib.get_user_cache_dir() + '/' + Misc.appId
|
||||
);
|
||||
|
||||
return createDirPromise(dir);
|
||||
}
|
||||
|
||||
function createTempDirPromise()
|
||||
{
|
||||
const dir = Gio.File.new_for_path(
|
||||
GLib.get_tmp_dir() + '/' + Misc.appId
|
||||
);
|
||||
|
||||
return createDirPromise(dir);
|
||||
}
|
||||
|
||||
/* Creates dir and resolves with it */
|
||||
function createDirPromise(dir)
|
||||
{
|
||||
return new Promise((resolve, reject) => {
|
||||
if(dir.query_exists(null))
|
||||
return resolve(dir);
|
||||
|
||||
dir.make_directory_async(
|
||||
GLib.PRIORITY_DEFAULT,
|
||||
null
|
||||
)
|
||||
.then(success => {
|
||||
if(success)
|
||||
return resolve(dir);
|
||||
|
||||
reject(new Error(`could not create dir: ${dir.get_path()}`));
|
||||
})
|
||||
.catch(err => reject(err));
|
||||
});
|
||||
}
|
||||
|
||||
/* Saves file in optional subdirectory and resolves with it */
|
||||
function saveFilePromise(place, subdirName, fileName, data)
|
||||
{
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let folderPath = GLib[`get_${place}_dir`]() + '/' + Misc.appId;
|
||||
|
||||
if(subdirName)
|
||||
folderPath += `/${subdirName}`;
|
||||
|
||||
const destDir = Gio.File.new_for_path(folderPath);
|
||||
const destPath = folderPath + '/' + fileName;
|
||||
|
||||
debug(`saving file: ${destPath}`);
|
||||
|
||||
const checkFolders = (subdirName)
|
||||
? [destDir.get_parent(), destDir]
|
||||
: [destDir];
|
||||
|
||||
for(let dir of checkFolders) {
|
||||
const createdDir = await createDirPromise(dir).catch(debug);
|
||||
if(!createdDir)
|
||||
return reject(new Error(`could not create dir: ${dir.get_path()}`));
|
||||
}
|
||||
|
||||
const destFile = destDir.get_child(fileName);
|
||||
destFile.replace_contents_bytes_async(
|
||||
GLib.Bytes.new_take(data),
|
||||
null,
|
||||
false,
|
||||
Gio.FileCreateFlags.NONE,
|
||||
null
|
||||
)
|
||||
.then(() => {
|
||||
debug(`saved file: ${destPath}`);
|
||||
resolve(destFile);
|
||||
})
|
||||
.catch(err => reject(err));
|
||||
});
|
||||
}
|
||||
|
||||
function getFileContentsPromise(place, subdirName, fileName)
|
||||
{
|
||||
return new Promise((resolve, reject) => {
|
||||
let destPath = GLib[`get_${place}_dir`]() + '/' + Misc.appId;
|
||||
|
||||
if(subdirName)
|
||||
destPath += `/${subdirName}`;
|
||||
|
||||
destPath += `/${fileName}`;
|
||||
|
||||
const file = Gio.File.new_for_path(destPath);
|
||||
debug(`reading data from: ${destPath}`);
|
||||
|
||||
if(!file.query_exists(null)) {
|
||||
debug(`no such file: ${file.get_path()}`);
|
||||
return resolve(null);
|
||||
}
|
||||
|
||||
file.load_bytes_async(null)
|
||||
.then(result => {
|
||||
const data = result[0].get_data();
|
||||
if(!data || !data.length)
|
||||
return reject(new Error('source file is empty'));
|
||||
|
||||
debug(`read data from: ${destPath}`);
|
||||
|
||||
if(data instanceof Uint8Array)
|
||||
resolve(ByteArray.toString(data));
|
||||
else
|
||||
resolve(data);
|
||||
})
|
||||
.catch(err => reject(err));
|
||||
});
|
||||
}
|
@@ -151,7 +151,7 @@ class ClapperHeaderBarBase extends Gtk.Box
|
||||
|
||||
for(let name of layoutArr) {
|
||||
/* Menu might be named "appmenu" */
|
||||
if(!menuAdded && name === 'appmenu')
|
||||
if(!menuAdded && (!name || name === 'appmenu'))
|
||||
name = 'menu';
|
||||
|
||||
const widget = this[`${name}Widget`];
|
||||
|
@@ -1,5 +1,6 @@
|
||||
imports.gi.versions.Gdk = '4.0';
|
||||
imports.gi.versions.Gtk = '4.0';
|
||||
imports.gi.versions.Soup = '2.4';
|
||||
|
||||
const { Gst } = imports.gi;
|
||||
Gst.init(null);
|
||||
|
@@ -1,5 +1,6 @@
|
||||
imports.gi.versions.Gdk = '4.0';
|
||||
imports.gi.versions.Gtk = '4.0';
|
||||
imports.gi.versions.Soup = '2.4';
|
||||
|
||||
const { AppRemote } = imports.src.appRemote;
|
||||
const Misc = imports.src.misc;
|
||||
|
22
src/menu.js
22
src/menu.js
@@ -2,12 +2,18 @@ const { GObject, Gtk } = imports.gi;
|
||||
const Dialogs = imports.src.dialogs;
|
||||
|
||||
var actions = {
|
||||
openLocal: (window) => new Dialogs.FileChooser(window),
|
||||
openUri: (window) => new Dialogs.UriDialog(window),
|
||||
prefs: (window) => new Dialogs.PrefsDialog(window),
|
||||
about: (window) => new Dialogs.AboutDialog(window),
|
||||
openLocal: {
|
||||
run: (window) => new Dialogs.FileChooser(window),
|
||||
accels: ['<Ctrl>O'],
|
||||
},
|
||||
openUri: {
|
||||
run: (window) => new Dialogs.UriDialog(window),
|
||||
accels: ['<Ctrl>U'],
|
||||
},
|
||||
prefs: {
|
||||
run: (window) => new Dialogs.PrefsDialog(window),
|
||||
},
|
||||
about: {
|
||||
run: (window) => new Dialogs.AboutDialog(window),
|
||||
},
|
||||
};
|
||||
|
||||
var accels = [
|
||||
['app.quit', ['q']],
|
||||
];
|
||||
|
64
src/misc.js
64
src/misc.js
@@ -1,10 +1,13 @@
|
||||
const { Gio, GstAudio, Gdk, Gtk } = imports.gi;
|
||||
const { Gio, Gdk, Gtk } = imports.gi;
|
||||
const Debug = imports.src.debug;
|
||||
|
||||
const { debug } = Debug;
|
||||
|
||||
var appName = 'Clapper';
|
||||
var appId = 'com.github.rafostar.Clapper';
|
||||
var subsMimes = [
|
||||
'application/x-subrip',
|
||||
];
|
||||
|
||||
var clapperPath = null;
|
||||
var clapperVersion = null;
|
||||
@@ -95,3 +98,62 @@ function getFormattedTime(time, showHours)
|
||||
const parsed = (hours) ? `${hours}:` : '';
|
||||
return parsed + `${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
function parsePlaylistFiles(filesArray)
|
||||
{
|
||||
let index = filesArray.length;
|
||||
let subs = null;
|
||||
|
||||
while(index--) {
|
||||
const file = filesArray[index];
|
||||
const filename = (file.get_basename)
|
||||
? file.get_basename()
|
||||
: file.substring(file.lastIndexOf('/') + 1);
|
||||
|
||||
const [type, isUncertain] = Gio.content_type_guess(filename, null);
|
||||
|
||||
if(subsMimes.includes(type)) {
|
||||
subs = file;
|
||||
filesArray.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* We only support single video
|
||||
* with external subtitles */
|
||||
if(subs && filesArray.length > 1)
|
||||
subs = null;
|
||||
|
||||
return [filesArray, subs];
|
||||
}
|
||||
|
||||
function getFileFromLocalUri(uri)
|
||||
{
|
||||
const file = Gio.file_new_for_uri(uri);
|
||||
|
||||
if(!file.query_exists(null)) {
|
||||
debug(new Error(`file does not exist: ${file.get_path()}`));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
function encodeHTML(text)
|
||||
{
|
||||
return text.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function decodeURIPlus(uri)
|
||||
{
|
||||
return decodeURI(uri.replace(/\+/g, ' '));
|
||||
}
|
||||
|
||||
function isHex(num)
|
||||
{
|
||||
return Boolean(num.match(/[0-9a-f]+$/i));
|
||||
}
|
||||
|
118
src/player.js
118
src/player.js
@@ -2,6 +2,7 @@ const { Gdk, Gio, GObject, Gst, GstClapper, Gtk } = imports.gi;
|
||||
const ByteArray = imports.byteArray;
|
||||
const Debug = imports.src.debug;
|
||||
const Misc = imports.src.misc;
|
||||
const YouTube = imports.src.youtube;
|
||||
const { PlayerBase } = imports.src.playerBase;
|
||||
|
||||
const { debug } = Debug;
|
||||
@@ -15,14 +16,16 @@ class ClapperPlayer extends PlayerBase
|
||||
super._init();
|
||||
|
||||
this.seek_done = true;
|
||||
this.doneStartup = false;
|
||||
this.needsFastSeekRestore = false;
|
||||
this.customVideoTitle = null;
|
||||
|
||||
this.canAutoFullscreen = false;
|
||||
this.playOnFullscreen = false;
|
||||
this.quitOnStop = false;
|
||||
this.needsTocUpdate = true;
|
||||
|
||||
this.keyPressCount = 0;
|
||||
this.ytClient = null;
|
||||
|
||||
const keyController = new Gtk.EventControllerKey();
|
||||
keyController.connect('key-pressed', this._onWidgetKeyPressed.bind(this));
|
||||
@@ -40,20 +43,39 @@ class ClapperPlayer extends PlayerBase
|
||||
|
||||
set_uri(uri)
|
||||
{
|
||||
if(Gst.Uri.get_protocol(uri) !== 'file')
|
||||
return super.set_uri(uri);
|
||||
this.customVideoTitle = null;
|
||||
|
||||
let file = Gio.file_new_for_uri(uri);
|
||||
if(!file.query_exists(null)) {
|
||||
debug(`file does not exist: ${file.get_path()}`, 'LEVEL_WARNING');
|
||||
if(Gst.Uri.get_protocol(uri) !== 'file') {
|
||||
const [isYouTubeUri, videoId] = YouTube.checkYouTubeUri(uri);
|
||||
|
||||
if(!isYouTubeUri)
|
||||
return super.set_uri(uri);
|
||||
|
||||
if(!this.ytClient)
|
||||
this.ytClient = new YouTube.YouTubeClient();
|
||||
|
||||
this.ytClient.getPlaybackDataAsync(videoId)
|
||||
.then(data => {
|
||||
this.customVideoTitle = data.title;
|
||||
super.set_uri(data.uri);
|
||||
})
|
||||
.catch(debug);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const file = Misc.getFileFromLocalUri(uri);
|
||||
if(!file) {
|
||||
if(!this.playlistWidget.nextTrack())
|
||||
debug('set media reached end of playlist');
|
||||
|
||||
return;
|
||||
}
|
||||
if(uri.endsWith('.claps'))
|
||||
return this.load_playlist_file(file);
|
||||
if(uri.endsWith('.claps')) {
|
||||
this.load_playlist_file(file);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
super.set_uri(uri);
|
||||
}
|
||||
@@ -86,24 +108,18 @@ class ClapperPlayer extends PlayerBase
|
||||
this.set_playlist(playlist);
|
||||
}
|
||||
|
||||
_preparePlaylist(playlist)
|
||||
{
|
||||
this.playlistWidget.removeAll();
|
||||
|
||||
for(let source of playlist) {
|
||||
const uri = (source.get_uri != null)
|
||||
? source.get_uri()
|
||||
: Gst.uri_is_valid(source)
|
||||
? source
|
||||
: Gst.filename_to_uri(source);
|
||||
|
||||
this.playlistWidget.addItem(uri);
|
||||
}
|
||||
}
|
||||
|
||||
set_playlist(playlist)
|
||||
{
|
||||
this._preparePlaylist(playlist);
|
||||
if(this.state !== GstClapper.ClapperState.STOPPED)
|
||||
this.stop();
|
||||
|
||||
this.playlistWidget.removeAll();
|
||||
this.canAutoFullscreen = true;
|
||||
|
||||
for(let source of playlist) {
|
||||
const uri = this._getSourceUri(source);
|
||||
this.playlistWidget.addItem(uri);
|
||||
}
|
||||
|
||||
const firstTrack = this.playlistWidget.get_row_at_index(0);
|
||||
if(!firstTrack) return;
|
||||
@@ -113,9 +129,14 @@ class ClapperPlayer extends PlayerBase
|
||||
|
||||
set_subtitles(source)
|
||||
{
|
||||
const uri = (source.get_uri)
|
||||
? source.get_uri()
|
||||
: source;
|
||||
const uri = this._getSourceUri(source);
|
||||
|
||||
/* Check local file existence */
|
||||
if(
|
||||
Gst.Uri.get_protocol(uri) === 'file'
|
||||
&& !Misc.getFileFromLocalUri(uri)
|
||||
)
|
||||
return;
|
||||
|
||||
this.set_subtitle_uri(uri);
|
||||
this.set_subtitle_track_enabled(true);
|
||||
@@ -237,6 +258,7 @@ class ClapperPlayer extends PlayerBase
|
||||
case 'play':
|
||||
case 'pause':
|
||||
case 'set_playlist':
|
||||
case 'set_subtitles':
|
||||
this[action](value);
|
||||
break;
|
||||
case 'toggle_maximized':
|
||||
@@ -260,6 +282,15 @@ class ClapperPlayer extends PlayerBase
|
||||
}
|
||||
}
|
||||
|
||||
_getSourceUri(source)
|
||||
{
|
||||
return (source.get_uri != null)
|
||||
? source.get_uri()
|
||||
: Gst.uri_is_valid(source)
|
||||
? source
|
||||
: Gst.filename_to_uri(source);
|
||||
}
|
||||
|
||||
_performCloseCleanup(window)
|
||||
{
|
||||
window.disconnect(this.closeRequestSignal);
|
||||
@@ -277,21 +308,21 @@ class ClapperPlayer extends PlayerBase
|
||||
}
|
||||
/* If "quitOnStop" is set here it means that we are in middle of autoclosing */
|
||||
if(this.state !== GstClapper.ClapperState.STOPPED && !this.quitOnStop) {
|
||||
const playlistItem = this.playlistWidget.getActiveRow();
|
||||
|
||||
let resumeInfo = {};
|
||||
if(settings.get_boolean('resume-enabled')) {
|
||||
const resumeTitle = this.playlistWidget.getActiveFilename();
|
||||
if(playlistItem.isLocalFile && settings.get_boolean('resume-enabled')) {
|
||||
const resumeTime = Math.floor(this.position / 1000000000);
|
||||
const resumeDuration = this.duration / 1000000000;
|
||||
|
||||
/* Do not save resume info when title is too long (random URI),
|
||||
* video is very short, just started or almost finished */
|
||||
/* Do not save resume info when video is very short,
|
||||
* just started or almost finished */
|
||||
if(
|
||||
resumeTitle.length < 300
|
||||
&& resumeDuration > 60
|
||||
resumeDuration > 60
|
||||
&& resumeTime > 15
|
||||
&& resumeDuration - resumeTime > 20
|
||||
) {
|
||||
resumeInfo.title = resumeTitle;
|
||||
resumeInfo.title = playlistItem.filename;
|
||||
resumeInfo.time = resumeTime;
|
||||
resumeInfo.duration = resumeDuration;
|
||||
|
||||
@@ -371,13 +402,18 @@ class ClapperPlayer extends PlayerBase
|
||||
debug(`URI loaded: ${uri}`);
|
||||
this.needsTocUpdate = true;
|
||||
|
||||
if(!this.doneStartup) {
|
||||
this.doneStartup = true;
|
||||
if(this.canAutoFullscreen) {
|
||||
this.canAutoFullscreen = false;
|
||||
|
||||
if(settings.get_boolean('fullscreen-auto')) {
|
||||
const root = player.widget.get_root();
|
||||
const clapperWidget = root.get_child();
|
||||
if(!clapperWidget.isFullscreenMode) {
|
||||
/* Do not enter fullscreen when already in it
|
||||
* or when in floating mode */
|
||||
if(
|
||||
!clapperWidget.isFullscreenMode
|
||||
&& clapperWidget.controlsRevealer.reveal_child
|
||||
) {
|
||||
this.playOnFullscreen = true;
|
||||
root.fullscreen();
|
||||
|
||||
@@ -390,7 +426,7 @@ class ClapperPlayer extends PlayerBase
|
||||
|
||||
_onPlayerWarning(player, error)
|
||||
{
|
||||
debug(error.message, 'LEVEL_WARNING');
|
||||
debug(error.message);
|
||||
}
|
||||
|
||||
_onPlayerError(player, error)
|
||||
@@ -476,12 +512,6 @@ class ClapperPlayer extends PlayerBase
|
||||
case Gdk.KEY_F:
|
||||
clapperWidget.toggleFullscreen();
|
||||
break;
|
||||
case Gdk.KEY_Escape:
|
||||
if(clapperWidget.isFullscreenMode) {
|
||||
root = this.widget.get_root();
|
||||
root.unfullscreen();
|
||||
}
|
||||
break;
|
||||
case Gdk.KEY_q:
|
||||
case Gdk.KEY_Q:
|
||||
root = this.widget.get_root();
|
||||
|
@@ -151,9 +151,7 @@ class ClapperPlayerBase extends GstClapper.Clapper
|
||||
break;
|
||||
case 'render-shadows':
|
||||
root = this.widget.get_root();
|
||||
/* Editing theme of someone else app is taboo */
|
||||
if(!root || !root.isClapperApp)
|
||||
break;
|
||||
if(!root) break;
|
||||
|
||||
const gpuClass = 'gpufriendly';
|
||||
const renderShadows = settings.get_boolean(key);
|
||||
@@ -176,27 +174,10 @@ class ClapperPlayerBase extends GstClapper.Clapper
|
||||
debug(`set subtitle-video offset: ${value}`);
|
||||
break;
|
||||
case 'dark-theme':
|
||||
case 'brighter-sliders':
|
||||
root = this.widget.get_root();
|
||||
if(!root || !root.isClapperApp)
|
||||
break;
|
||||
if(!root) break;
|
||||
|
||||
const brightClass = 'brightscale';
|
||||
const isBrighter = root.has_css_class(brightClass);
|
||||
|
||||
if(key === 'dark-theme' && isBrighter && !settings.get_boolean(key)) {
|
||||
root.remove_css_class(brightClass);
|
||||
debug('remove brighter sliders');
|
||||
break;
|
||||
}
|
||||
|
||||
const setBrighter = settings.get_boolean('brighter-sliders');
|
||||
if(setBrighter === isBrighter)
|
||||
break;
|
||||
|
||||
action = (setBrighter) ? 'add' : 'remove';
|
||||
root[action + '_css_class'](brightClass);
|
||||
debug(`${action} brighter sliders`);
|
||||
root.application._onThemeChanged(Gtk.Settings.get_default());
|
||||
break;
|
||||
case 'play-flags':
|
||||
const initialFlags = this.pipeline.flags;
|
||||
|
@@ -23,17 +23,27 @@ class ClapperPlayerRemote extends GObject.Object
|
||||
const uris = [];
|
||||
|
||||
/* We can not send GioFiles via WebSocket */
|
||||
for(let source of playlist) {
|
||||
const uri = (source.get_uri != null)
|
||||
? source.get_uri()
|
||||
: source;
|
||||
|
||||
uris.push(uri);
|
||||
}
|
||||
for(let source of playlist)
|
||||
uris.push(this._getSourceUri(source));
|
||||
|
||||
this.webclient.sendMessage({
|
||||
action: 'set_playlist',
|
||||
value: uris
|
||||
});
|
||||
}
|
||||
|
||||
set_subtitles(source)
|
||||
{
|
||||
this.webclient.sendMessage({
|
||||
action: 'set_subtitles',
|
||||
value: this._getSourceUri(source)
|
||||
});
|
||||
}
|
||||
|
||||
_getSourceUri(source)
|
||||
{
|
||||
return (source.get_uri != null)
|
||||
? source.get_uri()
|
||||
: source;
|
||||
}
|
||||
});
|
||||
|
@@ -55,30 +55,25 @@ class ClapperPlaylistWidget extends Gtk.ListBox
|
||||
return true;
|
||||
}
|
||||
|
||||
getActiveRow()
|
||||
{
|
||||
return this.get_row_at_index(this.activeRowId);
|
||||
}
|
||||
|
||||
getActiveFilename()
|
||||
{
|
||||
const row = this.get_row_at_index(this.activeRowId);
|
||||
const row = this.getActiveRow();
|
||||
if(!row) return null;
|
||||
|
||||
return row.filename;
|
||||
}
|
||||
|
||||
/* FIXME: Remove once/if GstPlay(er) gets
|
||||
* less vague MediaInfo signals */
|
||||
getActiveIsLocalFile()
|
||||
{
|
||||
const row = this.get_row_at_index(this.activeRowId);
|
||||
if(!row) return null;
|
||||
|
||||
return row.isLocalFile;
|
||||
}
|
||||
|
||||
deactivateActiveItem()
|
||||
{
|
||||
if(this.activeRowId < 0)
|
||||
return;
|
||||
|
||||
const row = this.get_row_at_index(this.activeRowId);
|
||||
const row = this.getActiveRow();
|
||||
if(!row) return null;
|
||||
|
||||
const icon = row.child.get_first_child();
|
||||
|
@@ -329,9 +329,7 @@ class ClapperTweaksPage extends PrefsBase.Grid
|
||||
super._init();
|
||||
|
||||
this.addTitle('Appearance');
|
||||
const darkCheck = this.addCheckButton('Enable dark theme', 'dark-theme');
|
||||
const brighterCheck = this.addCheckButton('Make sliders brighter', 'brighter-sliders');
|
||||
darkCheck.bind_property('active', brighterCheck, 'visible', GObject.BindingFlags.SYNC_CREATE);
|
||||
this.addCheckButton('Enable dark theme', 'dark-theme');
|
||||
|
||||
this.addTitle('Performance');
|
||||
this.addCheckButton('Render window shadows', 'render-shadows');
|
||||
|
@@ -128,6 +128,9 @@ class ClapperRevealerTop extends CustomRevealer
|
||||
|
||||
this.set_child(revealerBox);
|
||||
|
||||
this.mediaTitle.bind_property('visible', this.endTime, 'visible',
|
||||
GObject.BindingFlags.DEFAULT
|
||||
);
|
||||
this.connect('notify::child-revealed', this._onTopRevealed.bind(this));
|
||||
}
|
||||
|
||||
@@ -151,18 +154,20 @@ class ClapperRevealerTop extends CustomRevealer
|
||||
return this.mediaTitle.visible;
|
||||
}
|
||||
|
||||
setTimes(currTime, endTime)
|
||||
setTimes(currTime, endTime, isEndKnown)
|
||||
{
|
||||
const now = currTime.format(this.timeFormat);
|
||||
const end = endTime.format(this.timeFormat);
|
||||
const endText = `Ends at: ${end}`;
|
||||
this.currentTime.label = now;
|
||||
|
||||
this.currentTime.set_label(now);
|
||||
this.endTime.set_label(endText);
|
||||
const end = (isEndKnown)
|
||||
? endTime.format(this.timeFormat)
|
||||
: 'unknown';
|
||||
|
||||
this.endTime.label = `Ends at: ${end}`;
|
||||
|
||||
/* Make sure that next timeout is always run after clock changes,
|
||||
* by delaying it for additional few milliseconds */
|
||||
const nextUpdate = 60002 - parseInt(currTime.get_seconds() * 1000);
|
||||
const nextUpdate = 60004 - parseInt(currTime.get_seconds() * 1000);
|
||||
debug(`updated current time: ${now}, ends at: ${end}`);
|
||||
|
||||
return nextUpdate;
|
||||
@@ -315,6 +320,8 @@ class ClapperControlsRevealer extends Gtk.Revealer
|
||||
{
|
||||
if(this.child_revealed) {
|
||||
const clapperWidget = this.root.child;
|
||||
if(!clapperWidget) return;
|
||||
|
||||
const [width, height] = this.root.get_default_size();
|
||||
|
||||
clapperWidget.player.widget.height_request = -1;
|
||||
|
131
src/widget.js
131
src/widget.js
@@ -1,9 +1,10 @@
|
||||
const { Gdk, GLib, GObject, Gst, GstClapper, Gtk } = imports.gi;
|
||||
const { Gdk, Gio, GLib, GObject, Gst, GstClapper, Gtk } = imports.gi;
|
||||
const { Controls } = imports.src.controls;
|
||||
const Debug = imports.src.debug;
|
||||
const Dialogs = imports.src.dialogs;
|
||||
const Misc = imports.src.misc;
|
||||
const { Player } = imports.src.player;
|
||||
const YouTube = imports.src.youtube;
|
||||
const Revealers = imports.src.revealers;
|
||||
|
||||
const { debug } = Debug;
|
||||
@@ -22,8 +23,6 @@ class ClapperWidget extends Gtk.Grid
|
||||
|
||||
this.posX = 0;
|
||||
this.posY = 0;
|
||||
|
||||
this.windowSize = JSON.parse(settings.get_string('window-size'));
|
||||
this.layoutWidth = 0;
|
||||
|
||||
this.isFullscreenMode = false;
|
||||
@@ -39,7 +38,6 @@ class ClapperWidget extends Gtk.Grid
|
||||
this._hideControlsTimeout = null;
|
||||
this._updateTimeTimeout = null;
|
||||
|
||||
this.needsTracksUpdate = true;
|
||||
this.needsCursorRestore = false;
|
||||
|
||||
this.overlay = new Gtk.Overlay();
|
||||
@@ -59,8 +57,6 @@ class ClapperWidget extends Gtk.Grid
|
||||
this.attach(this.overlay, 0, 0, 1, 1);
|
||||
this.attach(this.controlsRevealer, 0, 1, 1, 1);
|
||||
|
||||
this.mapSignal = this.connect('map', this._onMap.bind(this));
|
||||
|
||||
this.player = new Player();
|
||||
const playerWidget = this.player.widget;
|
||||
|
||||
@@ -74,6 +70,7 @@ class ClapperWidget extends Gtk.Grid
|
||||
);
|
||||
this.player.connect('position-updated', this._onPlayerPositionUpdated.bind(this));
|
||||
this.player.connect('duration-changed', this._onPlayerDurationChanged.bind(this));
|
||||
this.player.connect('media-info-updated', this._onMediaInfoUpdated.bind(this));
|
||||
|
||||
this.overlay.set_child(playerWidget);
|
||||
this.overlay.add_overlay(this.revealerTop);
|
||||
@@ -106,17 +103,15 @@ class ClapperWidget extends Gtk.Grid
|
||||
|
||||
const dropTarget = this._getDropTarget();
|
||||
playerWidget.add_controller(dropTarget);
|
||||
const dropTargetTop = this._getDropTarget();
|
||||
this.revealerTop.add_controller(dropTargetTop);
|
||||
}
|
||||
|
||||
revealControls(isAllowInput)
|
||||
{
|
||||
this._checkSetUpdateTimeInterval();
|
||||
|
||||
this.revealerTop.revealChild(true);
|
||||
this.revealerBottom.revealChild(true);
|
||||
|
||||
this._checkSetUpdateTimeInterval();
|
||||
|
||||
if(isAllowInput)
|
||||
this.setControlsCanFocus(true);
|
||||
|
||||
@@ -198,23 +193,24 @@ class ClapperWidget extends Gtk.Grid
|
||||
this.controlsBox.set_visible(!isOnTop);
|
||||
}
|
||||
|
||||
_updateMediaInfo()
|
||||
_onMediaInfoUpdated(player, mediaInfo)
|
||||
{
|
||||
const mediaInfo = this.player.get_media_info();
|
||||
if(!mediaInfo)
|
||||
return GLib.SOURCE_REMOVE;
|
||||
|
||||
/* Set titlebar media title */
|
||||
this.updateTitle(mediaInfo);
|
||||
|
||||
/* FIXME: replace number with Gst.CLOCK_TIME_NONE when GJS
|
||||
* can do UINT64: https://gitlab.gnome.org/GNOME/gjs/-/merge_requests/524 */
|
||||
const isLive = (mediaInfo.is_live() || player.duration === 18446744073709552000);
|
||||
this.isSeekable = (!isLive && mediaInfo.is_seekable());
|
||||
|
||||
/* Show/hide position scale on LIVE */
|
||||
const isLive = mediaInfo.is_live();
|
||||
this.isSeekable = mediaInfo.is_seekable();
|
||||
this.controls.setLiveMode(isLive, this.isSeekable);
|
||||
|
||||
/* Update remaining end time if visible */
|
||||
this.updateTime();
|
||||
|
||||
if(this.player.needsTocUpdate) {
|
||||
/* FIXME: Remove `get_toc` check after required GstPlay(er) ver bump */
|
||||
if(!isLive && mediaInfo.get_toc)
|
||||
if(!isLive)
|
||||
this.updateChapters(mediaInfo.get_toc());
|
||||
|
||||
this.player.needsTocUpdate = false;
|
||||
@@ -287,7 +283,7 @@ class ClapperWidget extends Gtk.Grid
|
||||
|
||||
if(currStream && type !== 'subtitle') {
|
||||
const caps = currStream.get_caps();
|
||||
debug(`${type} caps: ${caps.to_string()}`, 'LEVEL_INFO');
|
||||
debug(`${type} caps: ${caps.to_string()}`);
|
||||
}
|
||||
if(type === 'video') {
|
||||
const isShowVis = (parsedInfo[`${type}Tracks`].length === 0);
|
||||
@@ -310,20 +306,21 @@ class ClapperWidget extends Gtk.Grid
|
||||
anyButtonShown = true;
|
||||
}
|
||||
this.controls.revealTracksRevealer.set_visible(anyButtonShown);
|
||||
|
||||
return GLib.SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
updateTitle(mediaInfo)
|
||||
{
|
||||
let title = mediaInfo.get_title();
|
||||
|
||||
if(!title) {
|
||||
const subtitle = this.player.playlistWidget.getActiveFilename();
|
||||
if(!title)
|
||||
title = this.player.customVideoTitle;
|
||||
|
||||
title = (subtitle.includes('.'))
|
||||
? subtitle.split('.').slice(0, -1).join('.')
|
||||
: subtitle;
|
||||
if(!title) {
|
||||
const item = this.player.playlistWidget.getActiveRow();
|
||||
|
||||
title = (item.isLocalFile && item.filename.includes('.'))
|
||||
? item.filename.split('.').slice(0, -1).join('.')
|
||||
: item.filename;
|
||||
}
|
||||
|
||||
this.root.title = title;
|
||||
@@ -333,11 +330,11 @@ class ClapperWidget extends Gtk.Grid
|
||||
|
||||
updateTime()
|
||||
{
|
||||
const revealerTop = this.revealerTop;
|
||||
|
||||
if(
|
||||
!revealerTop.visible
|
||||
|| !revealerTop.revealerGrid.visible
|
||||
!this.revealerTop.visible
|
||||
|| !this.revealerTop.revealerGrid.visible
|
||||
|| !this.isFullscreenMode
|
||||
|| this.isMobileMonitor
|
||||
)
|
||||
return null;
|
||||
|
||||
@@ -345,7 +342,7 @@ class ClapperWidget extends Gtk.Grid
|
||||
const endTime = currTime.add_seconds(
|
||||
this.controls.positionAdjustment.get_upper() - this.controls.currentPosition
|
||||
);
|
||||
const nextUpdate = this.revealerTop.setTimes(currTime, endTime);
|
||||
const nextUpdate = this.revealerTop.setTimes(currTime, endTime, this.isSeekable);
|
||||
|
||||
return nextUpdate;
|
||||
}
|
||||
@@ -447,16 +444,13 @@ class ClapperWidget extends Gtk.Grid
|
||||
this.controls.positionScale.clear_marks();
|
||||
this.controls.chapters = null;
|
||||
}
|
||||
if(!player.playlistWidget.getActiveIsLocalFile()) {
|
||||
this.needsTracksUpdate = true;
|
||||
}
|
||||
break;
|
||||
case GstClapper.ClapperState.STOPPED:
|
||||
debug('player state changed to: STOPPED');
|
||||
this.controls.currentPosition = 0;
|
||||
this.controls.positionScale.set_value(0);
|
||||
this.revealerTop.showTitle = false;
|
||||
this.controls.togglePlayButton.setPrimaryIcon();
|
||||
this.needsTracksUpdate = true;
|
||||
break;
|
||||
case GstClapper.ClapperState.PAUSED:
|
||||
debug('player state changed to: PAUSED');
|
||||
@@ -465,20 +459,10 @@ class ClapperWidget extends Gtk.Grid
|
||||
case GstClapper.ClapperState.PLAYING:
|
||||
debug('player state changed to: PLAYING');
|
||||
this.controls.togglePlayButton.setSecondaryIcon();
|
||||
if(this.needsTracksUpdate) {
|
||||
this.needsTracksUpdate = false;
|
||||
GLib.idle_add(
|
||||
GLib.PRIORITY_DEFAULT_IDLE,
|
||||
this._updateMediaInfo.bind(this)
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const isNotStopped = (state !== GstClapper.ClapperState.STOPPED);
|
||||
this.revealerTop.endTime.set_visible(isNotStopped);
|
||||
}
|
||||
|
||||
_onPlayerDurationChanged(player, duration)
|
||||
@@ -565,20 +549,17 @@ class ClapperWidget extends Gtk.Grid
|
||||
this.controls._onPlayerResize(width, height);
|
||||
}
|
||||
|
||||
_onMap()
|
||||
_onWindowMap(window)
|
||||
{
|
||||
this.disconnect(this.mapSignal);
|
||||
|
||||
const root = this.get_root();
|
||||
const surface = root.get_surface();
|
||||
const monitor = root.display.get_monitor_at_surface(surface);
|
||||
const surface = window.get_surface();
|
||||
const monitor = window.display.get_monitor_at_surface(surface);
|
||||
const geometry = monitor.geometry;
|
||||
const size = this.windowSize;
|
||||
const size = JSON.parse(settings.get_string('window-size'));
|
||||
|
||||
debug(`monitor application-pixels: ${geometry.width}x${geometry.height}`);
|
||||
|
||||
if(geometry.width >= size[0] && geometry.height >= size[1]) {
|
||||
root.set_default_size(size[0], size[1]);
|
||||
window.set_default_size(size[0], size[1]);
|
||||
debug(`restored window size: ${size[0]}x${size[1]}`);
|
||||
}
|
||||
|
||||
@@ -718,9 +699,11 @@ class ClapperWidget extends Gtk.Grid
|
||||
{
|
||||
const dropTarget = new Gtk.DropTarget({
|
||||
actions: Gdk.DragAction.COPY,
|
||||
preload: true,
|
||||
});
|
||||
dropTarget.set_gtypes([GObject.TYPE_STRING]);
|
||||
dropTarget.connect('drop', this._onDataDrop.bind(this));
|
||||
dropTarget.connect('notify::value', this._onDropValueNotify.bind(this));
|
||||
|
||||
return dropTarget;
|
||||
}
|
||||
@@ -894,17 +877,49 @@ class ClapperWidget extends Gtk.Grid
|
||||
this.posY = posY;
|
||||
}
|
||||
|
||||
_onDropValueNotify(dropTarget)
|
||||
{
|
||||
if(!dropTarget.value)
|
||||
return;
|
||||
|
||||
const uris = dropTarget.value.split(/\r?\n/);
|
||||
const firstUri = uris[0];
|
||||
|
||||
if(uris.length > 1 || !Gst.uri_is_valid(firstUri))
|
||||
return;
|
||||
|
||||
/* Check if user is dragging a YouTube link */
|
||||
const [isYouTubeUri, videoId] = YouTube.checkYouTubeUri(firstUri);
|
||||
if(!isYouTubeUri) return;
|
||||
|
||||
/* Since this is a YouTube video,
|
||||
* create YT client if it was not created yet */
|
||||
if(!this.player.ytClient)
|
||||
this.player.ytClient = new YouTube.YouTubeClient();
|
||||
|
||||
const { ytClient } = this.player;
|
||||
|
||||
/* Speed up things by prefetching new video info before drop */
|
||||
if(
|
||||
!ytClient.compareLastVideoId(videoId)
|
||||
&& ytClient.downloadingVideoId !== videoId
|
||||
)
|
||||
ytClient.getVideoInfoPromise(videoId).catch(debug);
|
||||
}
|
||||
|
||||
_onDataDrop(dropTarget, value, x, y)
|
||||
{
|
||||
const playlist = value.split(/\r?\n/).filter(uri => {
|
||||
const files = value.split(/\r?\n/).filter(uri => {
|
||||
return Gst.uri_is_valid(uri);
|
||||
});
|
||||
|
||||
if(!playlist.length)
|
||||
if(!files.length)
|
||||
return false;
|
||||
|
||||
this.player.set_playlist(playlist);
|
||||
this.root.application.activate();
|
||||
for(let index in files)
|
||||
files[index] = Gio.File.new_for_uri(files[index]);
|
||||
|
||||
this.root.application.open(files, "");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
910
src/youtube.js
Normal file
910
src/youtube.js
Normal file
@@ -0,0 +1,910 @@
|
||||
const { GObject, Gst, Soup } = imports.gi;
|
||||
const Dash = imports.src.dash;
|
||||
const Debug = imports.src.debug;
|
||||
const FileOps = imports.src.fileOps;
|
||||
const Misc = imports.src.misc;
|
||||
const YTDL = imports.src.assets['node-ytdl-core'];
|
||||
|
||||
const debug = Debug.ytDebug;
|
||||
const { settings } = Misc;
|
||||
|
||||
const InitAsyncState = {
|
||||
NONE: 0,
|
||||
IN_PROGRESS: 1,
|
||||
DONE: 2,
|
||||
};
|
||||
|
||||
var YouTubeClient = GObject.registerClass({
|
||||
Signals: {
|
||||
'info-resolved': {
|
||||
param_types: [GObject.TYPE_BOOLEAN]
|
||||
}
|
||||
}
|
||||
}, class ClapperYouTubeClient extends Soup.Session
|
||||
{
|
||||
_init()
|
||||
{
|
||||
super._init({
|
||||
timeout: 7,
|
||||
max_conns_per_host: 1,
|
||||
/* TODO: share this with GstClapper lib (define only once) */
|
||||
user_agent: 'Mozilla/5.0 (X11; Linux x86_64; rv:86.0) Gecko/20100101 Firefox/86.0',
|
||||
});
|
||||
this.initAsyncState = InitAsyncState.NONE;
|
||||
|
||||
/* videoID of current active download */
|
||||
this.downloadingVideoId = null;
|
||||
|
||||
this.lastInfo = null;
|
||||
this.postInfo = {
|
||||
clientVersion: null,
|
||||
visitorData: "",
|
||||
};
|
||||
|
||||
this.cachedSig = {
|
||||
id: null,
|
||||
actions: null,
|
||||
timestamp: "",
|
||||
};
|
||||
}
|
||||
|
||||
getVideoInfoPromise(videoId)
|
||||
{
|
||||
/* If in middle of download and same videoID,
|
||||
* resolve to current download */
|
||||
if(
|
||||
this.downloadingVideoId
|
||||
&& this.downloadingVideoId === videoId
|
||||
)
|
||||
return this._getCurrentDownloadPromise();
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
/* Do not redownload info for the same video */
|
||||
if(this.compareLastVideoId(videoId))
|
||||
return resolve(this.lastInfo);
|
||||
|
||||
this.abort();
|
||||
|
||||
/* Prevent doing this code more than once at a time */
|
||||
if(this.initAsyncState === InitAsyncState.NONE) {
|
||||
this.initAsyncState = InitAsyncState.IN_PROGRESS;
|
||||
|
||||
debug('loading cookies DB');
|
||||
const cacheDir = await FileOps.createCacheDirPromise().catch(debug);
|
||||
if(!cacheDir) {
|
||||
this.initAsyncState = InitAsyncState.NONE;
|
||||
return reject(new Error('could not create cookies DB'));
|
||||
}
|
||||
|
||||
const cookiesDB = new Soup.CookieJarDB({
|
||||
filename: cacheDir.get_child('cookies.sqlite').get_path(),
|
||||
read_only: false,
|
||||
});
|
||||
this.add_feature(cookiesDB);
|
||||
debug('successfully loaded cookies DB');
|
||||
|
||||
this.initAsyncState = InitAsyncState.DONE;
|
||||
}
|
||||
|
||||
/* Too many tries might trigger 429 ban,
|
||||
* leave while with break as a "goto" replacement */
|
||||
let tries = 1;
|
||||
while(tries--) {
|
||||
debug(`obtaining YouTube video info: ${videoId}`);
|
||||
this.downloadingVideoId = videoId;
|
||||
|
||||
let result;
|
||||
let isFoundInTemp = false;
|
||||
let isUsingPlayerResp = false;
|
||||
|
||||
const tempInfo = await FileOps.getFileContentsPromise('tmp', 'yt-info', videoId).catch(debug);
|
||||
if(tempInfo) {
|
||||
debug('checking temp info for requested video');
|
||||
let parsedTempInfo;
|
||||
|
||||
try { parsedTempInfo = JSON.parse(tempInfo); }
|
||||
catch(err) { debug(err); }
|
||||
|
||||
if(parsedTempInfo) {
|
||||
const nowSeconds = Math.floor(Date.now() / 1000);
|
||||
const { expireDate } = parsedTempInfo.streamingData;
|
||||
|
||||
if(expireDate && expireDate > nowSeconds) {
|
||||
debug(`found usable info, remaining live: ${expireDate - nowSeconds}`);
|
||||
|
||||
isFoundInTemp = true;
|
||||
result = { data: parsedTempInfo };
|
||||
}
|
||||
else
|
||||
debug('temp info expired');
|
||||
}
|
||||
}
|
||||
|
||||
if(!result)
|
||||
result = await this._getPlayerInfoPromise(videoId).catch(debug);
|
||||
if(!result || !result.data) {
|
||||
if(result && result.isAborted) {
|
||||
debug(new Error('download aborted'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
isUsingPlayerResp = (result != null);
|
||||
|
||||
if(!result)
|
||||
result = await this._getInfoPromise(videoId).catch(debug);
|
||||
if(!result || !result.data) {
|
||||
if(result && result.isAborted)
|
||||
debug(new Error('download aborted'));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if(!isFoundInTemp) {
|
||||
const [isPlayable, reason] = this._getPlayabilityStatus(result.data);
|
||||
|
||||
if(!isPlayable) {
|
||||
debug(new Error(reason));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let info = this._getReducedInfo(result.data);
|
||||
|
||||
if(this._getIsCipher(info.streamingData)) {
|
||||
debug('video requires deciphering');
|
||||
|
||||
/* Decipher actions do not change too often, so try
|
||||
* to reuse without triggering too many requests ban */
|
||||
let actions = this.cachedSig.actions;
|
||||
|
||||
if(actions)
|
||||
debug('using remembered decipher actions');
|
||||
else {
|
||||
let sts = "";
|
||||
const embedUri = `https://www.youtube.com/embed/${videoId}`;
|
||||
result = await this._downloadDataPromise(embedUri).catch(debug);
|
||||
|
||||
if(result && result.isAborted)
|
||||
break;
|
||||
else if(!result || !result.data) {
|
||||
debug(new Error('could not download embed body'));
|
||||
break;
|
||||
}
|
||||
|
||||
let ytPath = result.data.match(/jsUrl\":\"(.*?)\.js/g);
|
||||
if(ytPath) {
|
||||
ytPath = (ytPath[0] && ytPath[0].length > 16)
|
||||
? ytPath[0].substring(8) : null;
|
||||
}
|
||||
if(!ytPath) {
|
||||
debug(new Error('could not find YouTube player URI'));
|
||||
break;
|
||||
}
|
||||
const ytUri = `https://www.youtube.com${ytPath}`;
|
||||
if(
|
||||
/* check if site has "/" after ".com" */
|
||||
ytUri[23] !== '/'
|
||||
|| !Gst.Uri.is_valid(ytUri)
|
||||
) {
|
||||
debug(`misformed player URI: ${ytUri}`);
|
||||
break;
|
||||
}
|
||||
debug(`found player URI: ${ytUri}`);
|
||||
|
||||
const ytId = ytPath.split('/').find(el => Misc.isHex(el));
|
||||
let ytSigData = await FileOps.getFileContentsPromise(
|
||||
'user_cache', 'yt-sig', ytId
|
||||
).catch(debug);
|
||||
|
||||
if(ytSigData) {
|
||||
ytSigData = ytSigData.split(';');
|
||||
|
||||
if(ytSigData[0] && ytSigData[0] > 0) {
|
||||
sts = ytSigData[0];
|
||||
debug(`found local sts: ${sts}`);
|
||||
}
|
||||
|
||||
const actionsIndex = (ytSigData.length > 1) ? 1 : 0;
|
||||
actions = ytSigData[actionsIndex];
|
||||
}
|
||||
|
||||
if(!actions) {
|
||||
result = await this._downloadDataPromise(ytUri).catch(debug);
|
||||
|
||||
if(result && result.isAborted)
|
||||
break;
|
||||
else if(!result || !result.data) {
|
||||
debug(new Error('could not download player body'));
|
||||
break;
|
||||
}
|
||||
|
||||
const stsArr = result.data.match(/signatureTimestamp[=\:]\d+/g);
|
||||
if(stsArr) {
|
||||
sts = (stsArr[0] && stsArr[0].length > 19)
|
||||
? stsArr[0].substring(19) : null;
|
||||
|
||||
if(isNaN(sts) || sts <= 0)
|
||||
sts = "";
|
||||
else
|
||||
debug(`extracted player sts: ${sts}`);
|
||||
}
|
||||
|
||||
actions = YTDL.sig.extractActions(result.data);
|
||||
if(actions) {
|
||||
debug('deciphered, saving cipher actions to cache file');
|
||||
const saveData = sts + ';' + actions;
|
||||
/* We do not need to wait for it */
|
||||
FileOps.saveFilePromise('user_cache', 'yt-sig', ytId, saveData);
|
||||
}
|
||||
}
|
||||
if(!actions || !actions.length) {
|
||||
debug(new Error('could not extract decipher actions'));
|
||||
break;
|
||||
}
|
||||
if(this.cachedSig.id !== ytId) {
|
||||
this.cachedSig.id = ytId;
|
||||
this.cachedSig.actions = actions;
|
||||
this.cachedSig.timestamp = sts;
|
||||
|
||||
/* Cipher info from player without timestamp is invalid
|
||||
* so download it again now that we have a timestamp */
|
||||
if(isUsingPlayerResp && sts > 0) {
|
||||
debug(`redownloading player info with sts: ${sts}`);
|
||||
|
||||
result = await this._getPlayerInfoPromise(videoId).catch(debug);
|
||||
if(!result || !result.data) {
|
||||
if(result && result.isAborted)
|
||||
debug(new Error('download aborted'));
|
||||
|
||||
break;
|
||||
}
|
||||
info = this._getReducedInfo(result.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
debug(`successfully obtained decipher actions: ${actions}`);
|
||||
|
||||
const isDeciphered = this._decipherStreamingData(
|
||||
info.streamingData, actions
|
||||
);
|
||||
if(!isDeciphered) {
|
||||
debug('streaming data could not be deciphered');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!isFoundInTemp) {
|
||||
const exp = info.streamingData.expiresInSeconds || 0;
|
||||
const dateSeconds = Math.floor(Date.now() / 1000);
|
||||
|
||||
/* Estimated safe time for rewatching video */
|
||||
info.streamingData.expireDate = dateSeconds + Number(exp);
|
||||
|
||||
/* Last info is stored in variable, so don't wait here */
|
||||
FileOps.saveFilePromise(
|
||||
'tmp', 'yt-info', videoId, JSON.stringify(info)
|
||||
);
|
||||
}
|
||||
|
||||
this.lastInfo = info;
|
||||
debug('video info is ready to use');
|
||||
|
||||
this.emit('info-resolved', true);
|
||||
this.downloadingVideoId = null;
|
||||
|
||||
return resolve(info);
|
||||
}
|
||||
|
||||
/* Do not clear video info here, as we might still have
|
||||
* valid info from last video that can be reused */
|
||||
this.emit('info-resolved', false);
|
||||
this.downloadingVideoId = null;
|
||||
|
||||
reject(new Error('could not obtain YouTube video info'));
|
||||
});
|
||||
}
|
||||
|
||||
async getPlaybackDataAsync(videoId)
|
||||
{
|
||||
const info = await this.getVideoInfoPromise(videoId).catch(debug);
|
||||
|
||||
if(!info)
|
||||
throw new Error('no YouTube video info');
|
||||
|
||||
let uri = null;
|
||||
const dashInfo = await this.getDashInfoAsync(info).catch(debug);
|
||||
|
||||
if(dashInfo) {
|
||||
debug('parsed video info to dash info');
|
||||
const dash = Dash.generateDash(dashInfo);
|
||||
|
||||
if(dash) {
|
||||
debug('got dash data');
|
||||
|
||||
const dashFile = await FileOps.saveFilePromise(
|
||||
'tmp', null, 'clapper.mpd', dash
|
||||
).catch(debug);
|
||||
|
||||
if(dashFile)
|
||||
uri = dashFile.get_uri();
|
||||
|
||||
debug('got dash file');
|
||||
}
|
||||
}
|
||||
|
||||
if(!uri)
|
||||
uri = this.getBestCombinedUri(info);
|
||||
|
||||
if(!uri)
|
||||
throw new Error('no YouTube video URI');
|
||||
|
||||
debug(`final URI: ${uri}`);
|
||||
|
||||
const title = (info.videoDetails && info.videoDetails.title)
|
||||
? Misc.decodeURIPlus(info.videoDetails.title)
|
||||
: videoId;
|
||||
|
||||
debug(`title: ${title}`);
|
||||
|
||||
return { uri, title };
|
||||
}
|
||||
|
||||
async getDashInfoAsync(info)
|
||||
{
|
||||
if(
|
||||
!info.streamingData
|
||||
|| !info.streamingData.adaptiveFormats
|
||||
|| !info.streamingData.adaptiveFormats.length
|
||||
)
|
||||
return null;
|
||||
|
||||
/* TODO: Options in prefs to set preferred video formats and adaptive streaming */
|
||||
const isAdaptiveEnabled = settings.get_boolean('yt-adaptive-enabled');
|
||||
const allowedFormats = {
|
||||
video: [
|
||||
133,
|
||||
134,
|
||||
135,
|
||||
136,
|
||||
137,
|
||||
298,
|
||||
299,
|
||||
],
|
||||
audio: [
|
||||
140,
|
||||
]
|
||||
};
|
||||
|
||||
const filteredStreams = {
|
||||
video: [],
|
||||
audio: [],
|
||||
};
|
||||
|
||||
for(let fmt of ['video', 'audio']) {
|
||||
debug(`filtering ${fmt} streams`);
|
||||
let index = allowedFormats[fmt].length;
|
||||
|
||||
while(index--) {
|
||||
const itag = allowedFormats[fmt][index];
|
||||
const foundStream = info.streamingData.adaptiveFormats.find(stream => (stream.itag == itag));
|
||||
if(foundStream) {
|
||||
/* Parse and convert mimeType string into object */
|
||||
foundStream.mimeInfo = this._getMimeInfo(foundStream.mimeType);
|
||||
|
||||
/* Sanity check */
|
||||
if(!foundStream.mimeInfo || foundStream.mimeInfo.content !== fmt) {
|
||||
debug(new Error(`mimeType parsing failed on stream: ${itag}`));
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Sort from worst to best */
|
||||
filteredStreams[fmt].unshift(foundStream);
|
||||
debug(`added ${fmt} itag: ${foundStream.itag}`);
|
||||
|
||||
if(!isAdaptiveEnabled)
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!filteredStreams[fmt].length) {
|
||||
debug(`dash info ${fmt} streams list is empty`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
debug('following redirects');
|
||||
|
||||
for(let fmtArr of Object.values(filteredStreams)) {
|
||||
for(let stream of fmtArr) {
|
||||
debug(`initial URL: ${stream.url}`);
|
||||
|
||||
const result = await this._downloadDataPromise(stream.url, 'HEAD').catch(debug);
|
||||
if(!result) return null;
|
||||
|
||||
stream.url = Misc.encodeHTML(result.uri)
|
||||
.replace('?', '/')
|
||||
.replace(/&/g, '/')
|
||||
.replace(/=/g, '/');
|
||||
|
||||
debug(`resolved URL: ${stream.url}`);
|
||||
}
|
||||
}
|
||||
|
||||
debug('all redirects resolved');
|
||||
|
||||
return {
|
||||
duration: info.videoDetails.lengthSeconds,
|
||||
adaptations: [
|
||||
filteredStreams.video,
|
||||
filteredStreams.audio,
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
getBestCombinedUri(info)
|
||||
{
|
||||
debug('obtaining best combined URL');
|
||||
|
||||
if(!info.streamingData.formats.length)
|
||||
return null;
|
||||
|
||||
const combinedStream = info.streamingData.formats[
|
||||
info.streamingData.formats.length - 1
|
||||
];
|
||||
|
||||
if(!combinedStream || !combinedStream.url)
|
||||
return null;
|
||||
|
||||
return combinedStream.url;
|
||||
}
|
||||
|
||||
compareLastVideoId(videoId)
|
||||
{
|
||||
if(!this.lastInfo)
|
||||
return false;
|
||||
|
||||
if(
|
||||
!this.lastInfo
|
||||
|| !this.lastInfo.videoDetails
|
||||
|| this.lastInfo.videoDetails.videoId !== videoId
|
||||
/* TODO: check if video expired */
|
||||
)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_downloadDataPromise(url, method, reqData)
|
||||
{
|
||||
method = method || 'GET';
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const message = Soup.Message.new(method, url);
|
||||
const result = {
|
||||
data: null,
|
||||
isAborted: false,
|
||||
uri: null,
|
||||
};
|
||||
|
||||
if(reqData) {
|
||||
message.set_request(
|
||||
"application/json",
|
||||
Soup.MemoryUse.COPY,
|
||||
reqData
|
||||
);
|
||||
}
|
||||
|
||||
this.queue_message(message, (session, msg) => {
|
||||
debug('got message response');
|
||||
const statusCode = msg.status_code;
|
||||
|
||||
if(statusCode === 200) {
|
||||
result.data = msg.response_body.data;
|
||||
|
||||
if(method === 'HEAD')
|
||||
result.uri = msg.uri.to_string(false);
|
||||
|
||||
return resolve(result);
|
||||
}
|
||||
|
||||
debug(new Error(`response code: ${statusCode}`));
|
||||
|
||||
/* Internal Soup codes mean download aborted
|
||||
* or some other error that cannot be handled
|
||||
* and we do not want to retry in such case */
|
||||
if(statusCode < 10 || statusCode === 429) {
|
||||
result.isAborted = true;
|
||||
return resolve(result);
|
||||
}
|
||||
|
||||
return reject(new Error('could not download data'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_getCurrentDownloadPromise()
|
||||
{
|
||||
debug('resolving after current download finishes');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const infoResolvedSignal = this.connect('info-resolved', (self, success) => {
|
||||
this.disconnect(infoResolvedSignal);
|
||||
|
||||
debug('current download finished, resolving');
|
||||
|
||||
if(!success)
|
||||
return reject(new Error('info resolve was unsuccessful'));
|
||||
|
||||
/* At this point new video info is set */
|
||||
resolve(this.lastInfo);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_getPlayabilityStatus(info)
|
||||
{
|
||||
if(
|
||||
!info.playabilityStatus
|
||||
|| !info.playabilityStatus.status === 'OK'
|
||||
)
|
||||
return [false, 'video is not playable'];
|
||||
|
||||
if(!info.streamingData)
|
||||
return [false, 'video response data is missing streaming data'];
|
||||
|
||||
return [true, null];
|
||||
}
|
||||
|
||||
_getReducedInfo(info)
|
||||
{
|
||||
const reduced = {
|
||||
videoDetails: {
|
||||
videoId: info.videoDetails.videoId,
|
||||
title: info.videoDetails.title,
|
||||
lengthSeconds: info.videoDetails.lengthSeconds,
|
||||
isLiveContent: info.videoDetails.isLiveContent
|
||||
},
|
||||
streamingData: info.streamingData
|
||||
};
|
||||
|
||||
/* Make sure we have all formats arrays,
|
||||
* so we will not have to keep checking */
|
||||
if(!reduced.streamingData.formats)
|
||||
reduced.streamingData.formats = [];
|
||||
if(!reduced.streamingData.adaptiveFormats)
|
||||
reduced.streamingData.adaptiveFormats = [];
|
||||
|
||||
return reduced;
|
||||
}
|
||||
|
||||
_getMimeInfo(mimeType)
|
||||
{
|
||||
debug(`parsing mimeType: ${mimeType}`);
|
||||
|
||||
const mimeArr = mimeType.split(';');
|
||||
|
||||
let codecs = mimeArr.find(info => info.includes('codecs')).split('=')[1];
|
||||
codecs = codecs.substring(1, codecs.length - 1);
|
||||
|
||||
const mimeInfo = {
|
||||
content: mimeArr[0].split('/')[0],
|
||||
type: mimeArr[0],
|
||||
codecs,
|
||||
};
|
||||
|
||||
debug(`parsed mimeType: ${JSON.stringify(mimeInfo)}`);
|
||||
|
||||
return mimeInfo;
|
||||
}
|
||||
|
||||
_getPlayerInfoPromise(videoId)
|
||||
{
|
||||
const data = this._getPlayerPostData(videoId);
|
||||
const apiKey = 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8';
|
||||
const url = `https://www.youtube.com/youtubei/v1/player?key=${apiKey}`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if(!data) {
|
||||
debug('not using player info due to missing data');
|
||||
return resolve(null);
|
||||
}
|
||||
debug('downloading info from player');
|
||||
|
||||
this._downloadDataPromise(url, 'POST', data).then(result => {
|
||||
if(result.isAborted)
|
||||
return resolve(result);
|
||||
|
||||
debug('parsing player info JSON');
|
||||
|
||||
let info = null;
|
||||
|
||||
try { info = JSON.parse(result.data); }
|
||||
catch(err) { debug(err.message); }
|
||||
|
||||
if(!info)
|
||||
return reject(new Error('could not parse video info JSON'));
|
||||
|
||||
debug('successfully parsed video info JSON');
|
||||
|
||||
/* Update post info values from response */
|
||||
if(info && info.responseContext && info.responseContext.visitorData) {
|
||||
const visData = info.responseContext.visitorData;
|
||||
|
||||
this.postInfo.visitorData = visData;
|
||||
debug(`new visitor ID: ${visData}`);
|
||||
}
|
||||
|
||||
result.data = info;
|
||||
resolve(result);
|
||||
})
|
||||
.catch(err => reject(err));
|
||||
});
|
||||
}
|
||||
|
||||
_getInfoPromise(videoId)
|
||||
{
|
||||
return new Promise((resolve, reject) => {
|
||||
const query = [
|
||||
`video_id=${videoId}`,
|
||||
`el=embedded`,
|
||||
`eurl=https://youtube.googleapis.com/v/${videoId}`,
|
||||
`sts=${this.cachedSig.timestamp}`,
|
||||
].join('&');
|
||||
const url = `https://www.youtube.com/get_video_info?${query}`;
|
||||
|
||||
debug('downloading info from video');
|
||||
|
||||
this._downloadDataPromise(url).then(result => {
|
||||
if(result.isAborted)
|
||||
return resolve(result);
|
||||
|
||||
debug('parsing video info JSON');
|
||||
|
||||
const gstUri = Gst.Uri.from_string('?' + result.data);
|
||||
|
||||
if(!gstUri)
|
||||
return reject(new Error('could not convert query to URI'));
|
||||
|
||||
const playerResponse = gstUri.get_query_value('player_response');
|
||||
const cliVer = gstUri.get_query_value('cver');
|
||||
|
||||
if(cliVer && cliVer !== this.postInfo.clientVersion) {
|
||||
this.postInfo.clientVersion = cliVer;
|
||||
debug(`updated client version: ${cliVer}`);
|
||||
}
|
||||
|
||||
if(!playerResponse)
|
||||
return reject(new Error('no player response in query'));
|
||||
|
||||
let info = null;
|
||||
|
||||
try { info = JSON.parse(playerResponse); }
|
||||
catch(err) { debug(err.message); }
|
||||
|
||||
if(!info)
|
||||
return reject(new Error('could not parse video info JSON'));
|
||||
|
||||
debug('successfully parsed video info JSON');
|
||||
result.data = info;
|
||||
|
||||
resolve(result);
|
||||
})
|
||||
.catch(err => reject(err));
|
||||
});
|
||||
}
|
||||
|
||||
_getIsCipher(data)
|
||||
{
|
||||
/* Check only first best combined,
|
||||
* AFAIK there are no videos without it */
|
||||
if(data.formats[0].url)
|
||||
return false;
|
||||
|
||||
if(
|
||||
data.formats[0].signatureCipher
|
||||
|| data.formats[0].cipher
|
||||
)
|
||||
return true;
|
||||
|
||||
/* FIXME: no URLs and no cipher, what now? */
|
||||
debug(new Error('no url or cipher in streams'));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_decipherStreamingData(data, actions)
|
||||
{
|
||||
debug('checking cipher query keys');
|
||||
|
||||
/* Cipher query keys should be the same for all
|
||||
* streams, so parse any stream to get their names */
|
||||
const anyStream = data.formats[0] || data.adaptiveFormats[0];
|
||||
const sigQuery = anyStream.signatureCipher || anyStream.cipher;
|
||||
|
||||
if(!sigQuery)
|
||||
return false;
|
||||
|
||||
const gstUri = Gst.Uri.from_string('?' + sigQuery);
|
||||
const queryKeys = gstUri.get_query_keys();
|
||||
|
||||
const cipherKey = queryKeys.find(key => {
|
||||
const value = gstUri.get_query_value(key);
|
||||
/* A long value that is not URI */
|
||||
return (
|
||||
value.length > 32
|
||||
&& !Gst.Uri.is_valid(value)
|
||||
);
|
||||
});
|
||||
if(!cipherKey) {
|
||||
debug('no stream cipher key name');
|
||||
return false;
|
||||
}
|
||||
|
||||
const sigKey = queryKeys.find(key => {
|
||||
const value = gstUri.get_query_value(key);
|
||||
/* A short value that is not URI */
|
||||
return (
|
||||
value.length < 32
|
||||
&& !Gst.Uri.is_valid(value)
|
||||
);
|
||||
});
|
||||
if(!sigKey) {
|
||||
debug('no stream signature key name');
|
||||
return false;
|
||||
}
|
||||
|
||||
const urlKey = queryKeys.find(key =>
|
||||
Gst.Uri.is_valid(gstUri.get_query_value(key))
|
||||
);
|
||||
if(!urlKey) {
|
||||
debug('no stream URL key name');
|
||||
return false;
|
||||
}
|
||||
|
||||
const cipherKeys = {
|
||||
url: urlKey,
|
||||
sig: sigKey,
|
||||
cipher: cipherKey,
|
||||
};
|
||||
|
||||
debug('deciphering streams');
|
||||
|
||||
for(let format of [data.formats, data.adaptiveFormats]) {
|
||||
for(let stream of format) {
|
||||
const formatUrl = this._getDecipheredUrl(
|
||||
stream, actions, cipherKeys
|
||||
);
|
||||
if(!formatUrl) {
|
||||
debug('undecipherable stream');
|
||||
debug(stream);
|
||||
|
||||
return false;
|
||||
}
|
||||
stream.url = formatUrl;
|
||||
|
||||
/* Remove unneeded data */
|
||||
if(stream.signatureCipher)
|
||||
delete stream.signatureCipher;
|
||||
if(stream.cipher)
|
||||
delete stream.cipher;
|
||||
}
|
||||
}
|
||||
debug('all streams deciphered');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
_getDecipheredUrl(stream, actions, queryKeys)
|
||||
{
|
||||
debug(`deciphering stream id: ${stream.itag}`);
|
||||
|
||||
const sigQuery = stream.signatureCipher || stream.cipher;
|
||||
if(!sigQuery) return null;
|
||||
|
||||
const gstUri = Gst.Uri.from_string('?' + sigQuery);
|
||||
|
||||
const url = gstUri.get_query_value(queryKeys.url);
|
||||
const cipher = gstUri.get_query_value(queryKeys.cipher);
|
||||
const sig = gstUri.get_query_value(queryKeys.sig);
|
||||
|
||||
const key = YTDL.sig.decipher(cipher, actions);
|
||||
if(!key) return null;
|
||||
|
||||
debug('stream deciphered');
|
||||
|
||||
return `${url}&${sig}=${encodeURIComponent(key)}`;
|
||||
}
|
||||
|
||||
_getPlayerPostData(videoId)
|
||||
{
|
||||
const cliVer = this.postInfo.clientVersion;
|
||||
if(!cliVer) return null;
|
||||
|
||||
const visitor = this.postInfo.visitorData;
|
||||
const sts = this.cachedSig.timestamp || null;
|
||||
|
||||
const ua = this.user_agent;
|
||||
const browserVer = ua.substring(ua.lastIndexOf('/') + 1);
|
||||
|
||||
if(!visitor)
|
||||
debug('visitor ID is unknown');
|
||||
|
||||
const data = {
|
||||
videoId: videoId,
|
||||
context: {
|
||||
client: {
|
||||
visitorData: visitor,
|
||||
userAgent: `${ua},gzip(gfe)`,
|
||||
clientName: "WEB",
|
||||
clientVersion: cliVer,
|
||||
osName: "X11",
|
||||
osVersion: "",
|
||||
originalUrl: `https://www.youtube.com/watch?v=${videoId}`,
|
||||
browserName: "Firefox",
|
||||
browserVersion: browserVer,
|
||||
playerType: "UNIPLAYER"
|
||||
},
|
||||
user: {
|
||||
lockedSafetyMode: false
|
||||
},
|
||||
request: {
|
||||
useSsl: true,
|
||||
internalExperimentFlags: [],
|
||||
consistencyTokenJars: []
|
||||
}
|
||||
},
|
||||
playbackContext: {
|
||||
contentPlaybackContext: {
|
||||
html5Preference: "HTML5_PREF_WANTS",
|
||||
lactMilliseconds: "1069",
|
||||
referer: `https://www.youtube.com/watch?v=${videoId}`,
|
||||
signatureTimestamp: sts,
|
||||
autoCaptionsDefaultOn: false,
|
||||
liveContext: {
|
||||
startWalltime: "0"
|
||||
}
|
||||
}
|
||||
},
|
||||
captionParams: {}
|
||||
};
|
||||
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
});
|
||||
|
||||
function checkYouTubeUri(uri)
|
||||
{
|
||||
const gstUri = Gst.Uri.from_string(uri);
|
||||
const originalHost = gstUri.get_host();
|
||||
gstUri.normalize();
|
||||
|
||||
const host = gstUri.get_host();
|
||||
let videoId = null;
|
||||
|
||||
switch(host) {
|
||||
case 'www.youtube.com':
|
||||
case 'youtube.com':
|
||||
videoId = gstUri.get_query_value('v');
|
||||
if(!videoId) {
|
||||
/* Handle embedded videos */
|
||||
const segments = gstUri.get_path_segments();
|
||||
if(segments && segments.length)
|
||||
videoId = segments[segments.length - 1];
|
||||
}
|
||||
break;
|
||||
case 'youtu.be':
|
||||
videoId = gstUri.get_path_segments()[1];
|
||||
break;
|
||||
default:
|
||||
const scheme = gstUri.get_scheme();
|
||||
if(scheme === 'yt' || scheme === 'youtube') {
|
||||
/* ID is case sensitive */
|
||||
videoId = originalHost;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const success = (videoId != null);
|
||||
|
||||
return [success, videoId];
|
||||
}
|
Reference in New Issue
Block a user