103 Commits
0.1.0 ... 0.2.0

Author SHA1 Message Date
Rafał Dzięgiel
4766efbbc4 0.2.0 2021-04-13 12:45:03 +02:00
Rafał Dzięgiel
28c1daf709 Save and offer resume only for local files
We do not know if the online URI still leads to the same file as before (or if different one leads to previous). Additionally we would also need to check if media is seekable. Disable for now to avoid problems.
2021-04-12 22:29:20 +02:00
Rafał Dzięgiel
aa49d25df5 Stop pipeline before replacing playlist 2021-04-12 19:28:12 +02:00
Rafał Dzięgiel
774687710f Add setting to enable YouTube adaptive streaming
For now hidden because other related YouTube settings like min/max resolution, codecs etc. are not done yet
2021-04-12 18:45:36 +02:00
Rafał Dzięgiel
901fc8d760 YT: try harder to find suitable DASH streams
Instead of searching for 1080p only, accept also other H.264 formats for DASH streaming
2021-04-12 17:41:42 +02:00
Rafał Dzięgiel
ab32b2dbbc API: emit media info updated signal after video info updates
This fixes problem with wrong video resolution reported in media info due to being emitted before values were updated.
2021-04-12 17:38:44 +02:00
Rafał Dzięgiel
24bb9f298b Avoid situations with no menu in headerbar
Some shells might want to show menu button outside of the app. We do not support that.
2021-04-12 17:30:00 +02:00
Rafostar
2efa3e0bf6 YT: fix non-working best combined URIs
Fix an undefined variable introduced during recent code cleanup
2021-04-11 16:34:02 +02:00
Rafostar
92e3f7d93c YT: move info ready debug message before signal emit
Otherwise it will appear in wrong order in debug output and be misleading
2021-04-11 15:44:08 +02:00
Rafostar
85804ea297 Move YT related functions from player to youtube script 2021-04-11 15:35:41 +02:00
Rafostar
7cf86e92eb YT: resolve redirects on the Clapper side
Instead of providing URIs directly to GStreamer, follow redirects and provide that final URI. With this change souphttpsrc will not have to go through redirects from the beginning for each video segment.
2021-04-11 14:46:08 +02:00
Rafostar
b5711b145b Set Soup import version to 2.4
With Soup 3.0 release, there is a possibility of having both on the host. Set used version to 2.4 to avoid warnings and compatibility errors for now.
2021-04-10 21:41:28 +02:00
Rafostar
9271392397 Change default window size
Controls panel height is now 3px less then before (as intended). Change default window size setting to make it look good with 16:9 video on fresh install.
2021-04-10 14:58:24 +02:00
Rafał Dzięgiel
175de5bd6d Flatpak: update FFmpeg to 4.4 2021-04-09 21:54:54 +02:00
Rafał Dzięgiel
7b97f29aaf Flatpak: fix GTK4 build script 2021-04-09 21:14:56 +02:00
Rafał Dzięgiel
8d7fb761f7 Always reset auto fullscreen boolean value
Otherwise we would keep checking the settings with each playlist file and accidentally entered fullscreen when that setting value was changed in middle of playback.
2021-04-09 19:58:33 +02:00
Rafał Dzięgiel
aec2166c11 Only auto fullscreen with new playlist
This fixes a bug when player would enter fullscreen when changing playlist item
2021-04-09 19:34:17 +02:00
Rafał Dzięgiel
c21b214477 Tweak how auto fullscreen option works
When auto fullscreen is enabled, enter fullscreen on each media load
unless player is in floating mode in which probably user wants it to remain.
2021-04-09 19:11:34 +02:00
Rafał Dzięgiel
d9939a94c2 Fix some error messages not being displayed 2021-04-09 18:49:44 +02:00
Rafał Dzięgiel
dafa2cfdf5 Fix missing file extension in online URIs 2021-04-09 18:16:02 +02:00
Rafał Dzięgiel
ebe72f20b5 Hide end time together with title when stopped 2021-04-09 17:54:48 +02:00
Rafał Dzięgiel
fa1455556b Treat media without duration as live content 2021-04-09 16:07:11 +02:00
Rafał Dzięgiel
8fb6b971fe Always try to update end time after new media info 2021-04-09 13:53:43 +02:00
Rafał Dzięgiel
1bf46a2f12 Fix top time not showing up on fullscreen startup 2021-04-09 11:37:19 +02:00
Rafał Dzięgiel
a39c67e5e7 Check if local subtitle file exists before loading it
Otherwise GStreamer will error out and playback will stop. Non-existing subtitle file is common on Flatpak when user tries to open/drop a file from a folder that container does not have access to.
2021-04-09 10:55:06 +02:00
Rafał Dzięgiel
a3f78432f8 Flatpak: update GTK4 to latest git
GTK 4.2.0 has some problems that were fixed recently. Update to post release git.
2021-04-08 20:56:50 +02:00
Rafał Dzięgiel
7a7a04554f Flatpak: remove glib-networking dependency
Runtime has now up-to-date version
2021-04-08 20:54:46 +02:00
Rafał Dzięgiel
93de3dc056 Flatpak: update runtime to 40 2021-04-08 20:52:35 +02:00
Rafał Dzięgiel
eda80f314e API: replace source-setup with element-setup callback
Otherwise user agent is only set for source elements and not for further pipeline elements (e.g. dashdemux => souphttpsrc)
2021-04-08 18:24:49 +02:00
Rafał Dzięgiel
b3e6890571 Flatpak: add dashdemux sdix range download patch 2021-04-08 17:19:22 +02:00
Rafał Dzięgiel
c767b3e4b2 Separate debug messages for YouTube 2021-04-08 12:09:42 +02:00
Rafał Dzięgiel
ec6157763b YT: do not try to get info again for current download 2021-04-08 11:05:20 +02:00
Rafał Dzięgiel
cca3077936 Fix startup window size on XOrg
The window size was restored too early which caused the window to be a little bigger then it should on each launch. Restore window size after that window was mapped.
2021-04-07 20:07:05 +02:00
Rafał Dzięgiel
b5e1b3ab86 Flatpak: workaround crashes in adaptive streaming on Intel GPUs #51 2021-04-07 17:43:32 +02:00
Rafał Dzięgiel
b15b94fc90 Convenient ways of opening external subtitles
Play video with external subtiles by:
* selecting and opening both video+subs from file chooser/manager
* dropping subtitles file on player window
* opening subtiles from file chooser/manager while video plays
* send their file path/uri to player via WebSocket message
2021-04-07 16:33:21 +02:00
Rafał Dzięgiel
28d8986072 Update .gitattributes 2021-04-07 08:56:30 +02:00
Rafał Dzięgiel
30a7229b33 API: add media info updated signal
Emit media info updated signal only when media info is initially created and when number/format of tracks changes later. This is needed for GUI to detect resolution change (adaptive streaming) or when user adds external subtitles to current video.
2021-04-06 18:49:08 +02:00
Rafał Dzięgiel
9502e062f4 Support "Default" theme
Adwaita was renamed to Default in latest GTK4 code.
We need to detect it and treat it the same way as we did for Adwaita.

More info in related GTK4 MR:
https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/3079
2021-04-02 11:43:36 +02:00
Rafał Dzięgiel
6a9c77dfad Fix file paths in debug messages 2021-04-01 22:31:54 +02:00
Rafał Dzięgiel
133cda1b41 Fix forgotten ByteArray import during moving code 2021-04-01 22:31:22 +02:00
Rafał Dzięgiel
e68a7fe31a Work around GioFile promisify bug caused by GLib 2.68 2021-04-01 22:05:43 +02:00
Rafał Dzięgiel
7f69bee11c Move all file operations code to single file 2021-04-01 21:58:50 +02:00
Rafał Dzięgiel
295af9fd24 Improve debug messages for file operations 2021-04-01 20:20:42 +02:00
Rafostar
d04297620b Apply controls offset for Adwaita icons only
Only some icons from Adwaita get size modifications. Others themes do not need this, so they should not have an offset applied.
2021-03-31 21:38:44 +02:00
Rafostar
43acfddb06 Sink: inform about used OpenGL(ES) version 2021-03-31 17:34:49 +02:00
Rafostar
d54781eda7 Revert "Sink: limit GL APIs to OpenGL3+ and GLES2+"
This reverts commit 3fd30e41bf.

Some pipeline elements might still need to use (and work) on
older OpenGL versions.
2021-03-31 16:51:50 +02:00
Rafał Dzięgiel
96e5c5aa7c Restore 5px button margins for TV mode
They were reduced too much recently, causing chapter marks to not fit properly onto the controls bar
2021-03-31 09:04:29 +02:00
Rafał Dzięgiel
66ce006f00 Move buttons margins to CSS and tweak them a little 2021-03-30 22:15:12 +02:00
Rafał Dzięgiel
3fd30e41bf Sink: limit GL APIs to OpenGL3+ and GLES2+
Only allow using OpenGL 3.2+ or OpenGL ES 2.0+. This is what GTK4 supports.

#59
2021-03-29 14:38:19 +02:00
Rafał Dzięgiel
a6316c940c YT: always use up to date timestamp 2021-03-26 11:52:32 +01:00
Rafał Dzięgiel
84d9cc7416 Add DBus init debug message 2021-03-26 11:42:41 +01:00
Rafał Dzięgiel
a3499e9b47 Remove unused variable 2021-03-24 21:20:15 +01:00
Rafał Dzięgiel
4a60e01131 Auto set brighter sliders on Adwaita dark only 2021-03-24 21:16:41 +01:00
Rafał Dzięgiel
b404eb2f56 Increase play/pause icon size only on Adwaita theme
Adwaita theme has unusually small media constrols icons, but other popular themes do not. Instead increasing those icons on every theme do it only for Adwaita where problem occurs.
2021-03-24 20:36:08 +01:00
Rafał Dzięgiel
1f18796e0d Make buttons separator black on all themes
Using default @borders value causes it to be white even when all OSD buttons are half transparent black.
2021-03-24 20:04:09 +01:00
Rafał Dzięgiel
254aa538a5 YT: fix expire calc for long movies
Do not multiply video length when calculating expiration date. Otherwise for very long movies we might end up with with a past date.
2021-03-24 20:02:19 +01:00
Rafał Dzięgiel
58cc45ec7d Support accels for menu items
Add support for keyboard shortcuts for menu items. Additionally enable a common Ctrl+O for opening new file(s) and Ctrl+U for opening an URI.

Closes #45
2021-03-24 14:07:20 +01:00
Rafał Dzięgiel
7a75c6d4ff Merge pull request #55 from Rafostar/gl-mods
Simplify GL Sink code
2021-03-22 08:55:04 +01:00
Rafał Dzięgiel
2e97fc362c Dash: add segment ranges only to streams that have them 2021-03-19 15:19:00 +01:00
Rafał Dzięgiel
d762a59cc4 YT: do not keep URI in temp data twice 2021-03-19 14:27:55 +01:00
Rafał Dzięgiel
b42843be1f YT: do not check playability of saved temp data
Saved video info is always playable, otherwise its not saved in first place.
2021-03-19 11:43:37 +01:00
Rafał Dzięgiel
6dc825dfb3 YT: reduce amount of temp data stored per video 2021-03-19 11:25:36 +01:00
Rafał Dzięgiel
e89b3599c9 Remove "escape" key handler
It was conflicting with GTK build-in escape key functionality for closing popovers
2021-03-19 10:29:44 +01:00
Rafał Dzięgiel
79e12a6e36 YT: support obtaining info from player API 2021-03-19 10:26:46 +01:00
Rafał Dzięgiel
36d4a5c848 YT: replace some URL chars with slashes instead encoding it
Avoid dashdemux string query omission bug by using URL path with slashes instead of query string
2021-03-18 09:40:46 +01:00
Rafał Dzięgiel
38e5bae199 Replace lookbehind regexp
This was only supported in latest GJS version
2021-03-18 09:28:00 +01:00
Rafał Dzięgiel
fcff4b4450 Store all temp files in app named temp subdirectory 2021-03-17 11:57:02 +01:00
Rafał Dzięgiel
4021745a56 YT: set the same user agent as in player client 2021-03-17 10:47:38 +01:00
Rafał Dzięgiel
bd20d305ba YT: store reusable alive info in temp folder 2021-03-17 10:38:39 +01:00
Rafał Dzięgiel
d9b35b7fb8 YT: try only once
Avoid triggering 429 ban, by not trying second time
2021-03-16 19:45:33 +01:00
Rafał Dzięgiel
f1e00434ba Fix reference to undefined object 2021-03-16 13:44:22 +01:00
Rafał Dzięgiel
918157be04 Cooooookies!!! 2021-03-16 13:12:44 +01:00
Rafał Dzięgiel
72b55939b4 YT: abort on 429 error 2021-03-16 10:33:31 +01:00
Rafał Dzięgiel
e0a3ef78db YT: pass download info using object intead of array 2021-03-16 10:20:01 +01:00
Rafał Dzięgiel
4f46a7eaa8 YT: handle embedded videos URIs 2021-03-15 20:15:24 +01:00
Rafał Dzięgiel
050ef440dc Merge pull request #54 from Rafostar/yt-cache
YouTube cache
2021-03-15 16:37:55 +01:00
Rafał Dzięgiel
a4d55f8114 YT: store and load decipher actions from Clapper cache dir 2021-03-15 16:35:36 +01:00
Rafał Dzięgiel
aa60c56a58 Treat "yt" and "youtube" URI schemes as YouTube videos
You can set URI to "yt://VIDEO_ID" for YouTube videos
2021-03-15 13:40:50 +01:00
Rafał Dzięgiel
8c307dc90f YT: save decipher actions only after successful deciphering 2021-03-15 13:14:41 +01:00
Rafał Dzięgiel
5b6141ee8c YT: do not check player ID if actions are cached 2021-03-15 13:07:12 +01:00
Rafał Dzięgiel
8f294604dc Remove drop target from top revealer
It is causing various issues unfortunately
2021-03-14 23:33:29 +01:00
Rafostar
06f8e5d259 YT: cache current decipher actions 2021-03-14 21:00:18 +01:00
Rafostar
6370e1126b YT: check if decipher produced result 2021-03-14 16:50:23 +01:00
Rafostar
270e59137d YT: check if player URI is valid 2021-03-14 16:39:22 +01:00
Rafostar
ec18ca989a YT: decipher videos with signatures
Increase amount of playable YouTube videos by deciphering the ones that require to do so.

Many thanks to "node-ytdl-core" devs for JS regular expressions needed for YouTube player parsing.
2021-03-14 15:51:19 +01:00
Rafał Dzięgiel
46d24536c0 Do not keep invalid YT video info 2021-03-12 15:10:18 +01:00
Rafał Dzięgiel
c89d488c30 Prefetch YouTube video info on hover
Speed up loading of YouTube videos by downloading and parsing their info before video is dropped into player.
2021-03-12 13:05:58 +01:00
Rafał Dzięgiel
01c26cbbc3 Small text formatting fix 2021-03-12 08:50:52 +01:00
Rafał Dzięgiel
4375077dbc Decode custom video title from info
The values in JSON info are URI encoded with "+" signs, add custom decode function that decodes them.
2021-03-11 18:49:08 +01:00
Rafał Dzięgiel
fceb8ff70a YouTube support. Closes #46 2021-03-11 17:34:54 +01:00
Rafał Dzięgiel
6dc37088cf Fix error when playback finishes during controls reveal animation 2021-03-08 10:37:40 +01:00
Rafostar
4e85f6b749 API: set some common user agent
Some internet sites might prevent us from access unless some sort of a user agent is set
2021-03-07 21:37:01 +01:00
Rafostar
0cd82b1b8a API: remove video sink plugin selection
Clapper only has and supports one video sink. I would rather replace it than forcing support for multiple plugins.
2021-03-06 22:09:27 +01:00
Rafostar
39da52dd62 Sink: unlock widget before setting queue
Let GTK handle setting queue resize/render on the widget. We are not accessing widget values at this time, so it can be unlocked. It will be locked back during the render.
2021-03-06 19:16:28 +01:00
Rafał Dzięgiel
9c12afbf80 Sink: rename into GstClapperGLSink
The customized GTK4 sink version has few differences from the one shipped as part of GStreamer. Rename custom sink to GstClapperGLSink to avoid confusion.
2021-03-06 00:24:31 +01:00
Rafał Dzięgiel
e3c9b112e2 Sink: remove gtkconfig header
Remove another leftover meant for GTK3 compatibility
2021-03-05 23:27:08 +01:00
Rafał Dzięgiel
13d675beff Sink: merge gtkwidget into single class
Same as with video sink. Clapper uses only one so no need for subclassing.
2021-03-05 23:23:34 +01:00
Rafał Dzięgiel
95c3845398 Sink: merge gstsink into single class
Clapper only uses single video sink. No need for subclassing it.
2021-03-05 21:06:03 +01:00
Rafał Dzięgiel
93549a67af Sink: remove ignore_alpha property
GTK4 no longer supports ignoring alpha
2021-03-05 19:26:29 +01:00
Rafał Dzięgiel
07fb0a9a46 Sink: remove GTK4 if-defs
Clapper works with GTK4, so reduce codebase by removing GTK3 leftovers
2021-03-05 19:02:14 +01:00
Rafał Dzięgiel
fe3fd32932 Sink: keep track of widget allocation size
Instead of obtaining allocation size on each frame draw, keep track of its current size and update value on its change.
2021-03-05 18:41:26 +01:00
Rafał Dzięgiel
637212f7e8 Sink: move GL drawing logic into single function
No need having it in another function with additional call if it is used only from single place.
2021-03-05 18:10:35 +01:00
Rafał Dzięgiel
8b2e63ac48 Update README.md 2021-02-28 14:56:19 +01:00
58 changed files with 4092 additions and 2908 deletions

4
.gitattributes vendored
View File

@@ -1 +1,3 @@
lib/* linguist-vendored
lib/**/* linguist-vendored
lib/**/**/* linguist-vendored
lib/gst/clapper/gtk4/* linguist-vendored=false

View File

@@ -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>

View File

@@ -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 {

View File

@@ -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">

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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;

View 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;
}

View 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__ */

View File

@@ -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 (&gtk_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 (&gtk_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 (&gtk_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 (&gtk_sink->v_info) > 0) {
*end = *start +
gst_util_uint64_scale_int (GST_SECOND,
GST_VIDEO_INFO_FPS_D (&gtk_sink->v_info),
GST_VIDEO_INFO_FPS_N (&gtk_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 (&gtk_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, &gtk_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;
}

View File

@@ -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__ */

View File

@@ -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);
}

View File

@@ -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__ */

File diff suppressed because it is too large Load Diff

View 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__ */

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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__ */

View File

@@ -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);
}

View File

@@ -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__ */

View File

@@ -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

View File

@@ -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: [

View File

@@ -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),

View File

@@ -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

View File

@@ -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",

View File

@@ -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

View File

@@ -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"
}
]
}

View File

@@ -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

View File

@@ -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"
}
]
}

View File

@@ -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"
}
]
}

View File

@@ -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"
}
]
}

View File

@@ -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

View File

@@ -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"
}
]
}

View File

@@ -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

View File

@@ -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);
}
});

View File

@@ -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');
}
});

View 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(',');
}

View File

@@ -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
View File

@@ -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
View 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;
}

View File

@@ -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',

View File

@@ -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');
}

View File

@@ -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
View 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));
});
}

View File

@@ -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`];

View File

@@ -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);

View File

@@ -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;

View File

@@ -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']],
];

View File

@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
function decodeURIPlus(uri)
{
return decodeURI(uri.replace(/\+/g, ' '));
}
function isHex(num)
{
return Boolean(num.match(/[0-9a-f]+$/i));
}

View File

@@ -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();

View File

@@ -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;

View File

@@ -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;
}
});

View File

@@ -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();

View File

@@ -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');

View File

@@ -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;

View File

@@ -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
View 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(/&amp;/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];
}