211 Commits
0.1.0 ... 0.3.0

Author SHA1 Message Date
Rafał Dzięgiel
37d2f7aebd 0.3.0 2021-06-18 15:04:01 +02:00
Rafał Dzięgiel
f2ac3b20a3 meson: Install new symbolic icon 2021-06-18 10:28:25 +02:00
Rafał Dzięgiel
ade60b93a4 Merge pull request #86 from SeaDve/master
Add symbolic icon
2021-06-18 10:25:42 +02:00
Dave Patrick
48bc96f074 Add symbolic icon 2021-06-18 08:59:46 +08:00
Rafał Dzięgiel
0d9cb91705 Update TODO.md 2021-06-17 17:20:42 +02:00
Rafał Dzięgiel
21ccab1cc2 Add option to keep showing last video frame after playback
Previously Clapper showed last frame, now it defaults to black screen, so add an option for users to choose what they like better
2021-06-16 15:34:36 +02:00
Rafał Dzięgiel
bea3b1670d API: Raise ignored duration changes to 250 milliseconds
We do not show milliseconds in GUI, so we should not try to handle stream gaps that short.
2021-06-16 11:07:50 +02:00
Rafał Dzięgiel
0d4d3f1a8c sink: Avoid props code duplication 2021-06-16 09:43:55 +02:00
Rafał Dzięgiel
fc525ffcb1 sink: EGL on X11 from GTK 4.3.1
Recent GTK 4.3.1 release already uses EGL on x11, so lower the version check to that version
2021-06-15 16:52:47 +02:00
Rafał Dzięgiel
6f1a5626bc API: get MPRIS with a lock
Otherwise it is not thread safe
2021-06-15 16:16:29 +02:00
Rafał Dzięgiel
fbe6a8804c yt: Set some initial player version
It does not have to be up-to-date and even if it fails, we have a fallback that will update it anyway
2021-06-08 16:54:20 +02:00
Rafał Dzięgiel
c0b92c190b mpris: Fix wrong object type
Fix a copy-paste bug
2021-06-05 22:24:26 +02:00
Rafał Dzięgiel
8fc64eaf73 sink: Automatically draw black when going to NULL state
Remove all workarounds including ignore_textures prop and draw black functions and handle that automatically in sink when going into NULL state.
2021-06-02 20:56:50 +02:00
Rafał Dzięgiel
0b7f31b7c2 API: Properly return a boolean instead of a number 2021-06-02 20:24:40 +02:00
Rafał Dzięgiel
2d4353aaec Append some common subtitle track titles
In order to not end up with multiple subitle tracks simply named for e.g. "English", add some common postfix to it when detected in track title.
2021-06-02 15:47:09 +02:00
Rafał Dzięgiel
7062af472b API: Add function to get subtitle track title 2021-06-02 11:08:30 +02:00
Rafał Dzięgiel
1f4698448a Detect used GStreamer plugin names
Allows seeing what plugins are used with GST_DEBUG=Clapper:4. This is also needed for yet to come functionality of setting elements props.
2021-06-01 21:57:14 +02:00
Rafał Dzięgiel
95c8316af6 Change Enter key help description
OSD makes more sense to users then controls as it also shows top title and time layers
2021-06-01 08:31:02 +02:00
Rafał Dzięgiel
06d9f302c2 Revert "sink: Use g_main_context_invoke_full for drawing"
Not much benefit from using this function and unlike g_idle_add_full it
does not allow to skip a frame when previous one was not finished (slow HW).

This reverts commit f8a7abe195.
2021-05-31 18:07:37 +02:00
Rafał Dzięgiel
6246777f06 Prefer custom title over media info one
Otherwise YT videos will all show filename of dash manifest as title
2021-05-31 17:46:19 +02:00
Rafostar
1f781716d7 Add env variable to display FPS 2021-05-30 11:14:51 +02:00
Rafał Dzięgiel
0d7ef22c88 Merge pull request #77 from Rafostar/mpris
Add MPRIS
2021-05-26 22:01:49 +02:00
Rafał Dzięgiel
57664f32da sink: Do not send the same cursor coords on each GUI redraw 2021-05-26 21:15:58 +02:00
Rafał Dzięgiel
f8a7abe195 sink: Use g_main_context_invoke_full for drawing
Queue draws to application (and GTK) main context using g_main_context_invoke_full method
2021-05-26 20:59:50 +02:00
Rafostar
5f259b28fe mpris: Add "SupportedUriSchemes" and handle "OpenUri" method 2021-05-26 15:14:28 +02:00
Rafostar
9f776e9ecb mpris: Support changing volume 2021-05-26 15:13:30 +02:00
Rafał Dzięgiel
edb799bafa API: Parse title from URI when no title in tags 2021-05-24 15:35:04 +02:00
Rafał Dzięgiel
7535c4e598 mpris: Support position reporting and seeking 2021-05-24 15:34:47 +02:00
Rafał Dzięgiel
f0475ee055 API: Support seeking by offset 2021-05-24 15:33:52 +02:00
Rafał Dzięgiel
68d7205ead mpris: Support metadata url, title and length 2021-05-24 15:33:38 +02:00
Rafał Dzięgiel
f08ffad178 Initial MPRIS support
Implement a working MPRIS DBus connection with a separate API to control it. Right now only player playback state is reflected and Play/Pause/PlayPause calls work.
2021-05-24 15:33:15 +02:00
Rafał Dzięgiel
c2de0b7b33 yt: Use "html5=1" request query string 2021-05-24 13:04:10 +02:00
Rafał Dzięgiel
ac7be5956c Update Flathub submodule 2021-05-20 18:55:17 +02:00
Rafał Dzięgiel
76a1efab58 flatpak: Build from local dir instead of git
Allows doing test builds with unmerged changes
2021-05-20 17:27:47 +02:00
Rafał Dzięgiel
a2bbd2708d sink: Support EGL under x11 with GTK 4.4+ 2021-05-14 18:11:58 +02:00
Rafał Dzięgiel
9e77660cac Mark extras dir contents as linguist-vendored 2021-05-13 21:28:35 +02:00
Rafał Dzięgiel
5ea22450c0 Use custom getUriProtocol function
Gst.Uri.get_protocol function is very simple. It just splits string by ":" and return the first part. We can do the same in JS and by doing that we do not have to initialize GStreamer just to get this function.
2021-05-13 21:24:28 +02:00
Rafał Dzięgiel
6ae38327ca Leave CSS fullscreen optimization applied
Do not apply and remove fullscreen optimization class when going/leaving fullscreen. Instead set it to be applied to fullscreen only with CSS.
2021-05-13 21:13:18 +02:00
Rafał Dzięgiel
9006e56534 Add "text/x-ssa" to list of known subtitle mimes
GStreamer does not do external .ass subs ATM, but we should treat them as subtitles anyway.
2021-05-13 21:01:11 +02:00
Rafał Dzięgiel
af0e082c43 Readapt to changed monitor on the fly
Check and apply/remove TV mode UI on the fly when switching monitors.
This allows for e.g. having a mobile device connected to external big screen,
drag Clapper window from one screen to another and UI should automatically
adapt between mobile and TV modes without interrupting playback.

This also helps in situations where monitor size is not initially known
on window map #74.
2021-05-12 15:31:15 +02:00
Rafostar
2f5d6d60ed API: add debug messages about dropped buffers 2021-05-06 14:34:08 +02:00
Rafał Dzięgiel
9d537c7318 Update TODO.md 2021-05-04 19:05:46 +02:00
Rafostar
970b1487ac Restore manual play call
Autoplay was causing some racy conditions when loaded with subtitle uri. Make it play after uri loaded signal, but still prevent going from stopped to play 2nd time.
2021-05-04 18:46:22 +02:00
Rafostar
fc51fd857c Fix missing actions shortcuts in main menu
Actions were loaded too early, causing GTK to not recognize them and show their keyboard shortcuts next to main menu items
2021-05-03 19:26:22 +02:00
Rafostar
b419ed7922 Add keyboard shortcuts window 2021-05-03 19:26:06 +02:00
Rafostar
4f69183b85 Flatpak: use libs and patches from Flathub as submodule
I do not want to maintain them in two different places
2021-05-03 14:12:05 +02:00
Rafostar
98df55b231 Do not forget to destroy file chooser 2021-05-02 22:20:51 +02:00
Rafostar
6a4f5f2560 Export playlist to file with Ctrl+E 2021-05-02 20:25:15 +02:00
Rafostar
efe9439633 Do not ref all dialogs
The bug that we try to workaround here affects only file chooser dialog and not every dialog, so do not increase ref count on them
2021-05-02 17:29:15 +02:00
Rafostar
4179176ce8 Sink: queue render on GTK settings change
We have turned off auto rendering in video widget, hence we need to manually refresh when user changes some GTK settings (theme, icons etc.)
2021-05-02 15:51:14 +02:00
Rafostar
a7288adf4c Do not check Ids length in shuffle repeat mode
We always have at least 2 playlist items, thus at least 1 Id in array when we reach this point in code
2021-05-01 12:27:56 +02:00
Rafostar
9bb3f999b1 Also seek to 0 for other repeat modes with 1 item playlists 2021-05-01 12:15:07 +02:00
Rafostar
0e6507682a Do not show visualizations button when no audio tracks 2021-05-01 12:11:16 +02:00
Rafał Dzięgiel
2d8471dea0 Add playlist shuffle repeat mode #52 2021-04-29 12:26:40 +02:00
Rafał Dzięgiel
68f49c1495 Replace media playlist playing icon with repeat button #52
Show current and toggle change of repeat mode inside the playlist popover. The previous currently playing icon did not reflect actual playing state, so this should be better and does not take more space in UI.
2021-04-29 11:44:07 +02:00
Rafał Dzięgiel
a8bb6c40f4 Add more WebSocket API actions 2021-04-28 12:40:27 +02:00
Rafał Dzięgiel
71db78a0f6 Use "window.close" action instead of "close-request" 2021-04-28 12:15:20 +02:00
Rafał Dzięgiel
4133557086 Do not send or apply undefined args over WebSocket 2021-04-28 11:50:14 +02:00
Rafał Dzięgiel
d926e6b389 Add keybinding to change repeat mode #52 2021-04-27 15:25:22 +02:00
Rafał Dzięgiel
fd2de8b9b6 Add repeat mode options to playlist #52 2021-04-27 14:50:54 +02:00
Rafał Dzięgiel
de65eee106 API: simplify playbin flags detect function 2021-04-27 12:28:59 +02:00
Rafał Dzięgiel
9b07ff7dc5 Mention that packages from my repo are unstable 2021-04-27 11:23:42 +02:00
Rafał Dzięgiel
047dd12fbb Restore initial GUI state after playback 2021-04-27 11:14:09 +02:00
Rafał Dzięgiel
3238270c0d Ignore duration changes below 1ms during playback 2021-04-27 10:43:13 +02:00
Rafał Dzięgiel
997e47b93c API: let client decide what to do on EOS #52 2021-04-27 09:24:13 +02:00
Rafał Dzięgiel
ec1d4619a7 API: make "state" into a property 2021-04-26 22:30:01 +02:00
Rafał Dzięgiel
f4e48c9f8c Sink: render black on READY_TO_NULL state change
Show black background when playback finishes
2021-04-26 20:42:00 +02:00
Rafał Dzięgiel
1da6b94efc API: simplify EOS handling
Do not try to play smart with EOS by seeking to beginning. This leads to various errors or crashes. Just signal it and stop afterwards.
2021-04-26 20:40:44 +02:00
Rafał Dzięgiel
e4335721be Simplify auto-fullscreen logic 2021-04-26 17:51:20 +02:00
Rafał Dzięgiel
45d2702e01 API: fix missing drop of signals inhibit 2021-04-26 17:36:28 +02:00
Rafał Dzięgiel
a8aca7b3c0 API: make it autoplay on the same context invoke 2021-04-26 14:47:42 +02:00
Rafał Dzięgiel
c6e8824e3b API: add toggle_play method 2021-04-26 14:21:33 +02:00
Rafostar
e92ad68220 Print a warning when plugin rank cannot be changed 2021-04-25 22:00:00 +02:00
Rafostar
a98ca53dfb Use Gio.SimpleAction as only keypress handler 2021-04-25 20:19:44 +02:00
Rafał Dzięgiel
32995fc6a6 Sort chapters arr when switching to prev/next one
TOC representation obtained for some video files might be out of order. Sort them when switching between chapers, otherwise "next" chapter might not be the nearest one.
2021-04-22 15:48:16 +02:00
Rafał Dzięgiel
6b5240ddbc Add missing return value 2021-04-22 15:35:16 +02:00
Rafał Dzięgiel
46ef6bcd1d Use "const" for chapters keys
Array is reversed but variable holding it is not replaced in this function, so "const" can be used here
2021-04-22 15:28:19 +02:00
Rafał Dzięgiel
bd13a3c15a Use Shift+Left/Right to switch video chapters 2021-04-22 14:40:21 +02:00
Rafał Dzięgiel
edfa85b5cc Use Ctrl+Left/Right to switch playlist items. Closes #63 2021-04-22 14:32:02 +02:00
Rafał Dzięgiel
084f78a851 Change actions naming scheme
Use _ instead of capital letters in words for actions names. This will make some other stuff much easier.
2021-04-22 14:29:40 +02:00
Rafał Dzięgiel
c9b2f25192 Act on key press, not release 2021-04-22 14:13:53 +02:00
Rafał Dzięgiel
6f39b3939a Do not get ancestor on key release if unneeded 2021-04-21 18:14:13 +02:00
Rafał Dzięgiel
ee78ffb1e4 Fix seeks when window tiling with Super key
Super key is consumed by shell and never reaches app key press detection. Use that to fix seeking when tiling window by ignoring all key releases that did not have a key press beforehand.
2021-04-21 17:04:09 +02:00
Rafał Dzięgiel
bfbbc517d5 Small cleanup 2021-04-21 16:59:06 +02:00
Rafał Dzięgiel
7559a61c9f Hold Ctrl while doing D&D to append items to playlist instead of replacing it 2021-04-21 14:56:26 +02:00
Rafał Dzięgiel
deb273179f Add append_playlist function 2021-04-21 14:55:20 +02:00
Rafał Dzięgiel
231af36ef6 Enumerate local directories only 2021-04-20 19:31:24 +02:00
Rafał Dzięgiel
2e892c923b Support opening folders with media files
D&D folder with videos onto Clapper window to play them as video playlist. If folder contains exactly one video and subtitle file, then that video will be played with subtitles automatically applied.
2021-04-20 18:44:53 +02:00
Rafał Dzięgiel
0ab0b66825 0.2.1 2021-04-19 13:06:40 +02:00
Rafał Dzięgiel
d901eb4712 Update README.md 2021-04-19 10:31:30 +02:00
Rafał Dzięgiel
fe03719b38 Show tooltip with full playlist item text on hover
Some titles might be more than few words and will not fit in current playlist popover. Instead of stretching it, show full playlist item filename (or path) on hover in a tooltip.
2021-04-18 18:44:16 +02:00
Rafał Dzięgiel
f0ea7ae798 Remove set_seek_mode check
We now use a custom GstPlayer fork that has it added
2021-04-18 15:28:55 +02:00
Rafał Dzięgiel
380236b8ba Cleanup: do not extend player class twice
We only use the base class once, no need to have it separately then. Merge into single file.
2021-04-18 15:25:02 +02:00
Rafał Dzięgiel
e721130a63 YT: live videos with duration are not live anymore 2021-04-18 14:13:30 +02:00
Rafał Dzięgiel
eaf090d2e2 YT: be a little more quiet about some errors
Some errors are to be expected for some videos. Quietly use fallback methods for them without printing those errors.
2021-04-18 14:04:53 +02:00
Rafał Dzięgiel
87115f43d7 YT: store adaptive option value in itag opts
So its easier to access and obtained only once
2021-04-17 20:35:15 +02:00
Rafał Dzięgiel
33a5ec18fa Change prefs adaptive streaming text
This option sets the preferred streaming mode. When unavailable, other might still be used as a fallback.
2021-04-17 18:06:34 +02:00
Rafał Dzięgiel
ab8cafa0b8 YT: support non-adaptive live streaming 2021-04-17 18:03:33 +02:00
Rafał Dzięgiel
62b6de6db2 YT: support live HLS videos 2021-04-17 16:14:21 +02:00
Rafał Dzięgiel
643c2029d0 Fix wrong indentation size
All the other code uses 4 spaces indent
2021-04-17 13:12:58 +02:00
Rafał Dzięgiel
9799783ee5 Use Gst.(M)SECOND constants instead of numbers
It makes code easier to read
2021-04-17 13:08:12 +02:00
Rafał Dzięgiel
457cbde25e Remove unused return value
This function already appends to passed array. No need to return it.
2021-04-16 11:03:44 +02:00
Rafał Dzięgiel
2fd94fdc70 Add some YouTube related preferences 2021-04-16 10:37:17 +02:00
Rafał Dzięgiel
3a998fb91e YT: auto select best matching resolution for used monitor 2021-04-16 09:53:21 +02:00
Rafał Dzięgiel
b02f54a3a6 Do not show mobile controls transition on launch
Start app with the correct controls layout instead of showing the "hide elapsed time"
transition when started on mobile width. It is annoying.

We cannot detect surface width during app widgets assembly, so update the controls
revealers state on first surface update after window is mapped and only if running
on mobile width. Otherwise do not do anything like before which will result in
showing fully revealed controls (default).
2021-04-15 15:27:28 +02:00
Rafał Dzięgiel
ca7b44092e API: do not lock when changing scaled size values
Those values are private and should be accessed only from GTK thread, so locking widget should not be necessary here.
2021-04-15 11:58:12 +02:00
Rafał Dzięgiel
adbcfecb5e API: unset needs_info_update when stopped 2021-04-15 11:30:55 +02:00
Rafał Dzięgiel
a717e481e8 Fix missing top left menu buttons. Fixes #66
On some non-default system configurations the "menu" layout item might be replaced with one named "icon". Handle "icon" the same as "menu" when organizing headerbar buttons.
2021-04-14 17:48:57 +02:00
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
109 changed files with 6907 additions and 4741 deletions

6
.gitattributes vendored
View File

@@ -1 +1,5 @@
lib/* linguist-vendored
extras/**/* linguist-vendored
lib/**/* linguist-vendored
lib/**/**/* linguist-vendored
lib/gst/clapper/gstclapper-mpris* linguist-vendored=false
lib/gst/clapper/gtk4/* linguist-vendored=false

6
.gitmodules vendored
View File

@@ -1,3 +1,3 @@
[submodule "pkgs/flatpak/shared-modules"]
path = pkgs/flatpak/shared-modules
url = https://github.com/flathub/shared-modules.git
[submodule "pkgs/flatpak/flathub"]
path = pkgs/flatpak/flathub
url = https://github.com/flathub/com.github.rafostar.Clapper.git

View File

@@ -28,18 +28,21 @@ 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:
#### Debian, Fedora & openSUSE
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))
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)).<br>
Those are automatically build on each git commit, thus are considered unstable.
#### 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,11 +60,10 @@ 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>
Use either GitHub [discussions](https://github.com/Rafostar/clapper/discussions) or come and talk on Matrix: **#clapper-player:matrix.org**
Feel free to ask me any questions. Come and talk on Matrix: [#clapper-player:matrix.org](https://matrix.to/#/#clapper-player:matrix.org)
## Special Thanks
Many thanks to [sp1ritCS](https://github.com/sp1ritCS) for creating and maintaining package build files.

12
TODO.md
View File

@@ -15,11 +15,11 @@
- [X] Picture-in-Picture mode window (floating window)
- [ ] Touch gestures/swipes support
- Media playlists:
- [ ] Add more items to playlist via GUI
- [X] Add more items to playlist via D&D
- [X] Select video from playlist
- [ ] Reorder playlist items via D&D
- [X] Load special playlist file (.claps)
- [ ] Save to playlist file from GUI
- [X] Save to playlist file from GUI
- Seeking:
- [X] Customizable seek time
- [X] Set seek mode (default, accurate, fast)
@@ -31,7 +31,7 @@
- [X] Audio offset
- [ ] MDNS and UPNP (discovering media in local network)
- [X] DND files from Nautilus to play (ignore incompatible ones)
- [ ] Support dropping whole folders
- [X] Support dropping whole folders
- [ ] Search for subtitles, download and activate (SMplayer)
- [ ] Auto add subtitles from same folder
- [ ] Set global subtitles folders
@@ -40,7 +40,7 @@
- [X] Remote playback controls via HTTP (VLC) + WebSockets
- [ ] Expand available API
- [ ] API documentation
- [ ] Integration with the top bar
- [ ] MPRIS support
- [ ] Controls in the notifications panel
- [X] Integration with the top bar
- [X] MPRIS support
- [X] Controls in the notifications panel
- [ ] Progress bar in the notifications panel (maybe via extension)

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,23 +61,43 @@ radio {
.roundedcorners {
border-radius: 8px;
}
.adwthemedark scale trough highlight {
filter: brightness(120%);
}
.videowidget {
min-width: 320px;
min-height: 180px;
}
.tvmode popover box {
.fullscreen.tvmode popover box {
text-shadow: none;
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;
}
.fullscreen.tvmode .playercontrols button {
min-width: 32px;
min-height: 32px;
margin: 5px;
margin-left: 3px;
margin-right: 3px;
}
.fullscreen.tvmode button image {
-gtk-icon-shadow: none;
}
.tvmode radio {
.fullscreen.tvmode radio {
margin-left: 0px;
margin-right: 4px;
border: 2px solid;
@@ -85,40 +105,38 @@ radio {
min-height: 17px;
}
.tvmode .playercontrols {
.fullscreen.tvmode .playercontrols button image {
-gtk-icon-size: 24px;
}
.playbackicon {
.adwicons .playbackicon {
-gtk-icon-size: 20px;
}
.tvmode .playbackicon {
.adwicons.fullscreen.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;
.fullscreen.tvmode .labelbuttonlabel {
font-size: 22px;
text-shadow: none;
}
/* Top Revealer */
.tvmode .revealertopgrid {
.fullscreen.tvmode .revealertopgrid {
font-family: 'Cantarell', sans-serif;
}
.tvmode .tvtitle {
.fullscreen.tvmode .tvtitle {
font-size: 28px;
font-weight: 500;
text-shadow: none;
}
.tvtime {
.fullscreen.tvmode .tvtime {
margin-top: -2px;
margin-bottom: -2px;
min-height: 4px;
@@ -126,7 +144,7 @@ radio {
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.tvendtime {
.fullscreen.tvmode .tvendtime {
margin-top: -4px;
margin-bottom: 2px;
min-height: 6px;
@@ -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;
@@ -155,7 +171,7 @@ radio {
.osd .positionscale trough highlight {
min-height: 6px;
}
.tvmode .positionscale trough slider {
.fullscreen.tvmode .positionscale trough slider {
color: transparent;
background: transparent;
border-color: transparent;
@@ -167,11 +183,11 @@ radio {
.positionscale.fine-tune mark indicator {
min-height: 6px;
}
.tvmode .positionscale mark indicator {
.fullscreen.tvmode .positionscale mark indicator {
min-height: 7px;
min-width: 2px;
}
.tvmode .positionscale.fine-tune mark indicator {
.fullscreen.tvmode .positionscale.fine-tune mark indicator {
min-height: 7px;
min-width: 2px;
}
@@ -183,17 +199,17 @@ radio {
margin-top: 4px;
margin-bottom: -6px;
}
.tvmode .positionscale marks.top {
.fullscreen.tvmode .positionscale marks.top {
margin-bottom: 2px;
}
.tvmode .positionscale marks.bottom {
.fullscreen.tvmode .positionscale marks.bottom {
margin-top: 2px;
}
.tvmode .positionscale trough highlight {
.fullscreen.tvmode .positionscale trough highlight {
border-radius: 3px;
min-height: 20px;
}
.tvmode .positionscale.fine-tune trough highlight {
.fullscreen.tvmode .positionscale.fine-tune trough highlight {
border-radius: 3px;
min-height: 20px;
}
@@ -205,7 +221,7 @@ radio {
margin-right: -6px;
min-height: 180px;
}
.tvmode .volumescale {
.fullscreen.tvmode .volumescale {
margin: 2px;
margin-left: -6px;
margin-right: -4px;
@@ -216,7 +232,7 @@ radio {
margin-top: -4px;
margin-bottom: -6px;
}
.tvmode .volumescale trough highlight {
.fullscreen.tvmode .volumescale trough highlight {
min-width: 6px;
}
.overamp trough highlight {
@@ -230,10 +246,10 @@ radio {
.elapsedpopoverbox box separator {
background: @insensitive_fg_color;
}
.tvmode .elapsedpopoverbox {
.fullscreen.tvmode .elapsedpopoverbox {
min-width: 360px;
}
.tvmode .speedscale trough highlight {
.fullscreen.tvmode .speedscale trough highlight {
min-height: 6px;
}
@@ -256,7 +272,7 @@ radio {
.chapterlabel {
min-width: 32px;
}
.tvmode .chapterlabel {
.fullscreen.tvmode .chapterlabel {
min-width: 40px;
text-shadow: none;
font-size: 22px;
@@ -298,12 +314,9 @@ radio {
.gpufriendly {
box-shadow: -8px -8px transparent, 8px 8px transparent;
}
.gpufriendlyfs {
.fullscreen.gpufriendlyfs {
box-shadow: none;
}
.brightscale trough highlight {
filter: brightness(120%);
}
/* Error BG */
.blackbackground {

View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16"
height="16"
viewBox="0 0 4.2333333 4.2333334"
version="1.1"
id="svg5"
inkscape:version="1.1 (c68e22c387, 2021-05-23)"
sodipodi:docname="com.github.rafostar.Clapper-symbolic.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
inkscape:object-paths="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:snap-global="false"
units="px"
inkscape:zoom="32"
inkscape:cx="6.078125"
inkscape:cy="8.09375"
inkscape:window-width="1680"
inkscape:window-height="981"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect1853"
is_visible="true"
lpeversion="1"
satellites_param="F,0,0,1,0,1.8520833,0,1 @ F,0,0,1,0,1.8520833,0,1 @ F,0,0,1,0,1.8520833,0,1 @ F,0,0,1,0,1.8520833,0,1"
unit="px"
method="auto"
mode="F"
radius="7"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect1732"
is_visible="true"
lpeversion="1"
satellites_param="F,0,0,1,0,1.8520833,0,1 @ F,0,0,1,0,1.8520833,0,1 @ F,0,0,1,0,1.8520833,0,1 @ F,0,0,1,0,1.8520833,0,1"
unit="px"
method="auto"
mode="F"
radius="7"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g2022"
transform="matrix(0.06169519,0,0,0.06168906,-4.7800087,-3.2713603)">
<path
id="rect973"
style="fill:#000000;stroke-width:1.30776;stroke-linecap:round;stroke-linejoin:round;paint-order:stroke markers fill"
d="m 88.193064,81.795006 c -0.699254,0 -1.342327,0.227875 -1.864484,0.609782 h 51.32503 c -0.52216,-0.381907 -1.16471,-0.609782 -1.86397,-0.609782 z m -3.157945,10.475846 v 26.225278 c 0,1.74939 1.40856,3.15743 3.157945,3.15743 h 47.596576 c 1.74939,0 3.15795,-1.40804 3.15795,-3.15743 V 92.270852 Z m 20.323311,4.964038 15.40009,9.27283 -15.5205,9.56634 z" />
<path
style="fill:#000000;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 83.974394,69.464471 7.344033,-0.06509 a 2.7923103,2.7923103 33.047712 0 1 2.587466,1.683384 l 7.009937,16.201526 a 1.2163248,1.2163248 123.30899 0 1 -1.116623,1.699322 l -15.720141,-0.004 a 1.862525,1.862525 44.853691 0 1 -1.862019,-1.852534 l -0.08473,-15.79409 a 1.8585738,1.8585738 134.59241 0 1 1.842075,-1.868472 z"
id="path1422"
inkscape:path-effect="#path-effect1732"
inkscape:original-d="m 82.122383,69.480886 11.048055,-0.09792 8.480852,19.601124 -19.424307,-0.005 z" />
<rect
style="fill:#000000;stroke-width:1.3;stroke-linecap:round;stroke-linejoin:round;paint-order:stroke markers fill"
id="rect1544"
width="59.366463"
height="9.8661175"
x="82"
y="79.292183"
ry="1.2306831" />
<path
id="rect1847"
style="fill:#000000;stroke-width:4.91339;stroke-linecap:round;stroke-linejoin:round;paint-order:stroke markers fill"
d="m 522.12695,200.42773 c -0.45798,3.8e-4 -0.92335,0.0696 -1.38476,0.21289 l -172.88672,53.70313 5.28515,-0.0469 a 10.55362,10.55362 0 0 1 9.7793,6.36328 l 10.69922,24.72656 158.18359,-49.13477 c 2.46089,-0.7644 3.82691,-3.36137 3.0625,-5.82226 l -8.30078,-26.72657 c -0.62108,-1.99947 -2.4529,-3.277 -4.4375,-3.27539 z m -203.69531,63.05469 -3.08398,0.95899 c -2.46089,0.7644 -3.82691,3.35942 -3.0625,5.82031 l 6.29101,20.2539 z"
transform="scale(0.26458333)" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -14,6 +14,10 @@
<default>100</default>
<summary>Custom initial volume value in percentage after startup</summary>
</key>
<key name="keep-last-frame" type="b">
<default>false</default>
<summary>Keep showing last video frame after playback finishes</summary>
</key>
<key name="close-auto" type="b">
<default>false</default>
<summary>Automatically close the app after playback finishes</summary>
@@ -84,10 +88,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 +103,19 @@
<summary>Set PlayFlags for playbin</summary>
</key>
<!-- YouTube -->
<key name="yt-adaptive-enabled" type="b">
<default>false</default>
<summary>Enable to use adaptive streaming for YouTube</summary>
</key>
<key name="yt-quality-type" type="s">
<default>"hfr"</default>
<summary>Max YouTube video quality type</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

@@ -25,7 +25,7 @@
<p>
For best stability Wayland session is recommended. Wayland users with AMD/Intel GPUs
can try enabling HIGHLY EXPERIMENTAL "vah264dec" plugin inside player preferences
for reduced CPU and GPU usage on standard (8-bit) H.264 videos.
for reduced CPU and GPU usage on H.264 videos.
</p>
</description>
<developer_name>Rafał Dzięgiel</developer_name>
@@ -52,6 +52,64 @@
</screenshot>
</screenshots>
<releases>
<release version="0.3.0" date="2021-06-18">
<description>
<p>Changes:</p>
<ul>
<li>Added MPRIS support</li>
<li>Added repeat modes: single video, whole playlist and shuffle</li>
<li>Support opening folders with media files</li>
<li>Append playlist items by holding Ctrl while doing Drag and Drop</li>
<li>Improved handling of keyboard shortcuts</li>
<li>Added more keyboard shortcuts</li>
<li>Added window that shows available keyboard shortcuts</li>
<li>Show black screen by default after playback (make showing last frame optional instead)</li>
<li>Added ability to export playlist to file</li>
<li>Improve handling of changing displays with different resolutions</li>
<li>Added support for EGL under x11 with GTK 4.3.1 or later</li>
<li>Added missing symbolic app icon</li>
<li>Some misc bug fixes and code cleanups</li>
</ul>
</description>
</release>
<release version="0.2.1" date="2021-04-19">
<description>
<p>Player:</p>
<ul>
<li>Fix missing top left menu buttons on some system configurations</li>
<li>Fix potential video sink deadlock</li>
<li>Do not show mobile controls transition on launch</li>
<li>Show tooltip with full playlist item text on hover</li>
</ul>
<p>YouTube:</p>
<ul>
<li>Auto select best matching resolution for used monitor</li>
<li>Added some YouTube related preferences</li>
<li>Added support for live HLS videos</li>
<li>Added support for non-adaptive live HLS streaming</li>
</ul>
</description>
</release>
<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

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<node>
<interface name="org.mpris.MediaPlayer2">
<method name="Raise"/>
<method name="Quit"/>
<property name="CanQuit" type="b" access="read"/>
<property name="Fullscreen" type="b" access="readwrite"/>
<property name="CanSetFullscreen" type="b" access="read"/>
<property name="CanRaise" type="b" access="read"/>
<property name="HasTrackList" type="b" access="read"/>
<property name="Identity" type="s" access="read"/>
<property name="DesktopEntry" type="s" access="read"/>
<property name="SupportedUriSchemes" type="as" access="read"/>
<property name="SupportedMimeTypes" type="as" access="read"/>
</interface>
<interface name="org.mpris.MediaPlayer2.Player">
<method name="Next"/>
<method name="Previous"/>
<method name="Pause"/>
<method name="PlayPause"/>
<method name="Stop"/>
<method name="Play"/>
<method name="Seek">
<arg name="Offset" type="x" direction="in"/>
</method>
<method name="SetPosition">
<arg name="TrackId" type="o" direction="in"/>
<arg name="Position" type="x" direction="in"/>
</method>
<method name="OpenUri">
<arg name="Uri" type="s" direction="in"/>
</method>
<signal name="Seeked">
<arg type="x" name="Position"/>
</signal>
<property name="PlaybackStatus" type="s" access="read"/>
<property name="LoopStatus" type="s" access="readwrite"/>
<property name="Rate" type="d" access="readwrite"/>
<property name="Shuffle" type="b" access="readwrite"/>
<property name="Metadata" type="a{sv}" access="read"/>
<property name="Volume" type="d" access="readwrite"/>
<property name="Position" type="x" access="read"/>
<property name="MinimumRate" type="d" access="read"/>
<property name="MaximumRate" type="d" access="read"/>
<property name="CanGoNext" type="b" access="read"/>
<property name="CanGoPrevious" type="b" access="read"/>
<property name="CanPlay" type="b" access="read"/>
<property name="CanPause" type="b" access="read"/>
<property name="CanSeek" type="b" access="read"/>
<property name="CanControl" type="b" access="read"/>
</interface>
</node>

View File

@@ -4,6 +4,9 @@ iconsdir = join_paths(sharedir, 'icons', 'hicolor')
install_data('com.github.rafostar.Clapper.svg',
install_dir: join_paths(iconsdir, 'scalable', 'apps')
)
install_data('com.github.rafostar.Clapper-symbolic.svg',
install_dir: join_paths(iconsdir, 'symbolic', 'apps')
)
install_data('com.github.rafostar.Clapper.gschema.xml',
install_dir: join_paths(sharedir, 'glib-2.0', 'schemas')
)

View File

@@ -28,6 +28,7 @@
#include <gst/clapper/gstclapper-g-main-context-signal-dispatcher.h>
#include <gst/clapper/gstclapper-video-overlay-video-renderer.h>
#include <gst/clapper/gstclapper-visualization.h>
#include <gst/clapper/gstclapper-mpris.h>
#include <gst/clapper/gstclapper-gtk4-plugin.h>
#endif /* __CLAPPER_H__ */

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

@@ -45,6 +45,7 @@ struct _GstClapperSubtitleInfo
{
GstClapperStreamInfo parent;
gchar *title;
gchar *language;
};
@@ -108,7 +109,7 @@ struct _GstClapperMediaInfo
GList *video_stream_list;
GList *subtitle_stream_list;
GstClockTime duration;
GstClockTime duration;
};
struct _GstClapperMediaInfoClass

View File

@@ -379,6 +379,7 @@ gst_clapper_subtitle_info_finalize (GObject * object)
{
GstClapperSubtitleInfo *info = GST_CLAPPER_SUBTITLE_INFO (object);
g_free (info->title);
g_free (info->language);
G_OBJECT_CLASS (gst_clapper_subtitle_info_parent_class)->finalize (object);
@@ -392,6 +393,20 @@ gst_clapper_subtitle_info_class_init (GstClapperSubtitleInfoClass * klass)
gobject_class->finalize = gst_clapper_subtitle_info_finalize;
}
/**
* gst_clapper_subtitle_info_get_title:
* @info: a #GstClapperSubtitleInfo
*
* Returns: the title of the stream, or NULL if unknown.
*/
const gchar *
gst_clapper_subtitle_info_get_title (const GstClapperSubtitleInfo * info)
{
g_return_val_if_fail (GST_IS_CLAPPER_SUBTITLE_INFO (info), NULL);
return info->title;
}
/**
* gst_clapper_subtitle_info_get_language:
* @info: a #GstClapperSubtitleInfo
@@ -513,6 +528,8 @@ gst_clapper_subtitle_info_copy (GstClapperSubtitleInfo * ref)
GstClapperSubtitleInfo *ret;
ret = gst_clapper_subtitle_info_new ();
if (ref->title)
ret->title = g_strdup (ref->title);
if (ref->language)
ret->language = g_strdup (ref->language);
@@ -767,7 +784,8 @@ gst_clapper_media_info_get_toc (const GstClapperMediaInfo * info)
* gst_clapper_media_info_get_title:
* @info: a #GstClapperMediaInfo
*
* Returns: the media title.
* Returns: the media title. When metadata does not contain title,
* returns title parsed from URI.
*/
const gchar *
gst_clapper_media_info_get_title (const GstClapperMediaInfo * info)

View File

@@ -170,7 +170,10 @@ GST_CLAPPER_API
GType gst_clapper_subtitle_info_get_type (void);
GST_CLAPPER_API
const gchar * gst_clapper_subtitle_info_get_language (const GstClapperSubtitleInfo* info);
const gchar * gst_clapper_subtitle_info_get_title (const GstClapperSubtitleInfo *info);
GST_CLAPPER_API
const gchar * gst_clapper_subtitle_info_get_language (const GstClapperSubtitleInfo *info);
#define GST_TYPE_CLAPPER_MEDIA_INFO \
(gst_clapper_media_info_get_type())

View File

@@ -0,0 +1,43 @@
/*
* Copyright (C) 2021 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_MPRIS_PRIVATE_H__
#define __GST_CLAPPER_MPRIS_PRIVATE_H__
#include <gst/clapper/gstclapper-mpris.h>
#include <gst/clapper/gstclapper.h>
G_BEGIN_DECLS
G_GNUC_INTERNAL
void gst_clapper_mpris_set_clapper (GstClapperMpris *self, GstClapper *clapper,
GstClapperSignalDispatcher *signal_dispatcher);
G_GNUC_INTERNAL
void gst_clapper_mpris_set_media_info (GstClapperMpris *self, GstClapperMediaInfo *info);
G_GNUC_INTERNAL
void gst_clapper_mpris_set_playback_status (GstClapperMpris *self, const gchar *status);
G_GNUC_INTERNAL
void gst_clapper_mpris_set_position (GstClapperMpris *self, gint64 position);
G_END_DECLS
#endif /* __GST_CLAPPER_MPRIS_PRIVATE_H__ */

View File

@@ -0,0 +1,778 @@
/*
* Copyright (C) 2021 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 "gstclapper-mpris-gdbus.h"
#include "gstclapper-mpris.h"
#include "gstclapper-mpris-private.h"
#include "gstclapper-signal-dispatcher-private.h"
GST_DEBUG_CATEGORY_STATIC (gst_clapper_mpris_debug);
#define GST_CAT_DEFAULT gst_clapper_mpris_debug
#define MPRIS_DEFAULT_VOLUME 1.0
enum
{
PROP_0,
PROP_OWN_NAME,
PROP_ID_PATH,
PROP_IDENTITY,
PROP_DESKTOP_ENTRY,
PROP_DEFAULT_ART_URL,
PROP_VOLUME,
PROP_LAST
};
struct _GstClapperMpris
{
GObject parent;
GstClapperMprisMediaPlayer2 *base_skeleton;
GstClapperMprisMediaPlayer2Player *player_skeleton;
GstClapperSignalDispatcher *signal_dispatcher;
GstClapperMediaInfo *media_info;
guint name_id;
/* Properties */
gchar *own_name;
gchar *id_path;
gchar *identity;
gchar *desktop_entry;
gchar *default_art_url;
gboolean parse_media_info;
/* Current status */
gchar *playback_status;
gboolean can_play;
guint64 position;
GThread *thread;
GMutex lock;
GCond cond;
GMainContext *context;
GMainLoop *loop;
};
struct _GstClapperMprisClass
{
GObjectClass parent_class;
};
#define parent_class gst_clapper_mpris_parent_class
G_DEFINE_TYPE (GstClapperMpris, gst_clapper_mpris, G_TYPE_OBJECT);
static GParamSpec *param_specs[PROP_LAST] = { NULL, };
static void gst_clapper_mpris_set_property (GObject * object, guint prop_id,
const GValue * value, GParamSpec * pspec);
static void gst_clapper_mpris_get_property (GObject * object, guint prop_id,
GValue * value, GParamSpec * pspec);
static void gst_clapper_mpris_dispose (GObject * object);
static void gst_clapper_mpris_finalize (GObject * object);
static void gst_clapper_mpris_constructed (GObject * object);
static gpointer gst_clapper_mpris_main (gpointer data);
static void unregister (GstClapperMpris * self);
static void
gst_clapper_mpris_init (GstClapperMpris * self)
{
GST_DEBUG_CATEGORY_INIT (gst_clapper_mpris_debug, "ClapperMpris", 0,
"GstClapperMpris");
GST_TRACE_OBJECT (self, "Initializing");
self = gst_clapper_mpris_get_instance_private (self);
g_mutex_init (&self->lock);
g_cond_init (&self->cond);
self->context = g_main_context_new ();
self->loop = g_main_loop_new (self->context, FALSE);
self->base_skeleton = gst_clapper_mpris_media_player2_skeleton_new ();
self->player_skeleton = gst_clapper_mpris_media_player2_player_skeleton_new ();
self->name_id = 0;
self->own_name = NULL;
self->id_path = NULL;
self->identity = NULL;
self->desktop_entry = NULL;
self->default_art_url = NULL;
self->signal_dispatcher = NULL;
self->media_info = NULL;
self->parse_media_info = FALSE;
self->playback_status = g_strdup ("Stopped");
self->can_play = FALSE;
self->position = 0;
GST_TRACE_OBJECT (self, "Initialized");
}
static void
gst_clapper_mpris_class_init (GstClapperMprisClass * klass)
{
GObjectClass *gobject_class = (GObjectClass *) klass;
gobject_class->set_property = gst_clapper_mpris_set_property;
gobject_class->get_property = gst_clapper_mpris_get_property;
gobject_class->dispose = gst_clapper_mpris_dispose;
gobject_class->finalize = gst_clapper_mpris_finalize;
gobject_class->constructed = gst_clapper_mpris_constructed;
param_specs[PROP_OWN_NAME] =
g_param_spec_string ("own-name", "DBus own name",
"DBus name to own on connection",
NULL, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY |
G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
param_specs[PROP_ID_PATH] =
g_param_spec_string ("id-path", "DBus id path",
"A valid D-Bus path describing this player",
NULL, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY |
G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
param_specs[PROP_IDENTITY] =
g_param_spec_string ("identity", "Player name",
"A friendly name to identify the media player",
NULL, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY |
G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
param_specs[PROP_DESKTOP_ENTRY] =
g_param_spec_string ("desktop-entry", "Desktop entry filename",
"The basename of an installed .desktop file",
NULL, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY |
G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
param_specs[PROP_DEFAULT_ART_URL] =
g_param_spec_string ("default-art-url", "Default Art URL",
"Default art to show when media does not provide one",
NULL, G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY |
G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
param_specs[PROP_VOLUME] =
g_param_spec_double ("volume", "Volume", "Volume",
0, 1.5, MPRIS_DEFAULT_VOLUME, G_PARAM_READWRITE |
G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (gobject_class, PROP_LAST, param_specs);
}
static void
gst_clapper_mpris_set_property (GObject * object, guint prop_id,
const GValue * value, GParamSpec * pspec)
{
GstClapperMpris *self = GST_CLAPPER_MPRIS (object);
switch (prop_id) {
case PROP_OWN_NAME:
self->own_name = g_value_dup_string (value);
break;
case PROP_ID_PATH:
self->id_path = g_value_dup_string (value);
break;
case PROP_IDENTITY:
self->identity = g_value_dup_string (value);
break;
case PROP_DESKTOP_ENTRY:
self->desktop_entry = g_value_dup_string (value);
break;
case PROP_DEFAULT_ART_URL:
self->default_art_url = g_value_dup_string (value);
break;
case PROP_VOLUME:
g_object_set_property (G_OBJECT (self->player_skeleton), "volume", value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gst_clapper_mpris_get_property (GObject * object, guint prop_id,
GValue * value, GParamSpec * pspec)
{
GstClapperMpris *self = GST_CLAPPER_MPRIS (object);
switch (prop_id) {
case PROP_VOLUME:
g_object_get_property (G_OBJECT (self->player_skeleton), "volume", value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gst_clapper_mpris_dispose (GObject * object)
{
GstClapperMpris *self = GST_CLAPPER_MPRIS (object);
GST_TRACE_OBJECT (self, "Stopping main thread");
if (self->loop) {
g_main_loop_quit (self->loop);
if (self->thread != g_thread_self ())
g_thread_join (self->thread);
else
g_thread_unref (self->thread);
self->thread = NULL;
g_main_loop_unref (self->loop);
self->loop = NULL;
g_main_context_unref (self->context);
self->context = NULL;
}
G_OBJECT_CLASS (parent_class)->dispose (object);
}
static void
gst_clapper_mpris_finalize (GObject * object)
{
GstClapperMpris *self = GST_CLAPPER_MPRIS (object);
GST_TRACE_OBJECT (self, "Finalize");
g_free (self->own_name);
g_free (self->id_path);
g_free (self->identity);
g_free (self->desktop_entry);
g_free (self->default_art_url);
g_free (self->playback_status);
if (self->base_skeleton)
g_object_unref (self->base_skeleton);
if (self->player_skeleton)
g_object_unref (self->player_skeleton);
if (self->signal_dispatcher)
g_object_unref (self->signal_dispatcher);
if (self->media_info)
g_object_unref (self->media_info);
g_mutex_clear (&self->lock);
g_cond_clear (&self->cond);
G_OBJECT_CLASS (parent_class)->finalize (object);
}
static void
gst_clapper_mpris_constructed (GObject * object)
{
GstClapperMpris *self = GST_CLAPPER_MPRIS (object);
GST_TRACE_OBJECT (self, "Constructed");
g_mutex_lock (&self->lock);
self->thread = g_thread_new ("GstClapperMpris",
gst_clapper_mpris_main, self);
while (!self->loop || !g_main_loop_is_running (self->loop))
g_cond_wait (&self->cond, &self->lock);
g_mutex_unlock (&self->lock);
G_OBJECT_CLASS (parent_class)->constructed (object);
}
static gboolean
main_loop_running_cb (gpointer user_data)
{
GstClapperMpris *self = GST_CLAPPER_MPRIS (user_data);
GST_TRACE_OBJECT (self, "Main loop running now");
g_mutex_lock (&self->lock);
g_cond_signal (&self->cond);
g_mutex_unlock (&self->lock);
return G_SOURCE_REMOVE;
}
static gboolean
handle_play_cb (GstClapperMprisMediaPlayer2Player * player_skeleton,
GDBusMethodInvocation * invocation, gpointer user_data)
{
GstClapper *clapper = GST_CLAPPER (user_data);
GST_DEBUG ("Handle Play");
gst_clapper_play (clapper);
gst_clapper_mpris_media_player2_player_complete_play (player_skeleton, invocation);
return TRUE;
}
static gboolean
handle_pause_cb (GstClapperMprisMediaPlayer2Player * player_skeleton,
GDBusMethodInvocation * invocation, gpointer user_data)
{
GstClapper *clapper = GST_CLAPPER (user_data);
GST_DEBUG ("Handle Pause");
gst_clapper_pause (clapper);
gst_clapper_mpris_media_player2_player_complete_pause (player_skeleton, invocation);
return TRUE;
}
static gboolean
handle_play_pause_cb (GstClapperMprisMediaPlayer2Player * player_skeleton,
GDBusMethodInvocation * invocation, gpointer user_data)
{
GstClapper *clapper = GST_CLAPPER (user_data);
GST_DEBUG ("Handle PlayPause");
gst_clapper_toggle_play (clapper);
gst_clapper_mpris_media_player2_player_complete_play_pause (player_skeleton, invocation);
return TRUE;
}
static gboolean
handle_seek_cb (GstClapperMprisMediaPlayer2Player * player_skeleton,
GDBusMethodInvocation * invocation, gint64 offset, gpointer user_data)
{
GstClapper *clapper = GST_CLAPPER (user_data);
GST_DEBUG ("Handle Seek");
gst_clapper_seek_offset (clapper, offset * GST_USECOND);
gst_clapper_mpris_media_player2_player_complete_seek (player_skeleton, invocation);
return TRUE;
}
static gboolean
handle_set_position_cb (GstClapperMprisMediaPlayer2Player * player_skeleton,
GDBusMethodInvocation * invocation, const gchar * track_id,
gint64 position, gpointer user_data)
{
GstClapper *clapper = GST_CLAPPER (user_data);
GST_DEBUG ("Handle SetPosition");
gst_clapper_seek (clapper, position * GST_USECOND);
gst_clapper_mpris_media_player2_player_complete_set_position (player_skeleton, invocation);
return TRUE;
}
static gboolean
handle_open_uri_cb (GstClapperMprisMediaPlayer2Player * player_skeleton,
GDBusMethodInvocation * invocation, const gchar * uri,
gpointer user_data)
{
GstClapper *clapper = GST_CLAPPER (user_data);
GST_DEBUG ("Handle OpenUri");
/* FIXME: set one item playlist instead */
gst_clapper_set_uri (clapper, uri);
gst_clapper_mpris_media_player2_player_complete_open_uri (player_skeleton, invocation);
return TRUE;
}
static void
volume_notify_dispatch (gpointer user_data)
{
GstClapperMpris *self = user_data;
g_object_notify_by_pspec (G_OBJECT (self), param_specs[PROP_VOLUME]);
}
static void
handle_volume_notify_cb (G_GNUC_UNUSED GObject * obj,
G_GNUC_UNUSED GParamSpec * pspec, GstClapperMpris * self)
{
gst_clapper_signal_dispatcher_dispatch (self->signal_dispatcher, NULL,
volume_notify_dispatch, g_object_ref (self),
(GDestroyNotify) g_object_unref);
}
static void
unregister (GstClapperMpris * self)
{
if (!self->name_id)
return;
GST_DEBUG_OBJECT (self, "Unregister");
g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self->base_skeleton));
g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self->player_skeleton));
g_bus_unown_name (self->name_id);
self->name_id = 0;
}
static const gchar *
_get_mpris_trackid (GstClapperMpris * self)
{
/* TODO: Support more tracks */
return g_strdup_printf ("%s%s%i", self->id_path, "/Track/", 0);
}
static void
_set_supported_uri_schemes (GstClapperMpris * self)
{
const gchar *uri_schemes[96] = {};
GList *elements, *el;
guint index = 0;
elements = gst_element_factory_list_get_elements (
GST_ELEMENT_FACTORY_TYPE_SRC, GST_RANK_NONE);
for (el = elements; el != NULL; el = el->next) {
const gchar *const *protocols;
GstElementFactory *factory = GST_ELEMENT_FACTORY (el->data);
if (gst_element_factory_get_uri_type (factory) != GST_URI_SRC)
continue;
protocols = gst_element_factory_get_uri_protocols (factory);
if (protocols == NULL || *protocols == NULL)
continue;
while (*protocols != NULL) {
guint j = index;
while (j--) {
if (strcmp (uri_schemes[j], *protocols) == 0)
goto next;
}
uri_schemes[index] = *protocols;
GST_DEBUG_OBJECT (self, "Added supported URI scheme: %s", *protocols);
++index;
next:
++protocols;
}
}
gst_plugin_feature_list_free (elements);
gst_clapper_mpris_media_player2_set_supported_uri_schemes (
self->base_skeleton, uri_schemes);
}
static void
name_acquired_cb (GDBusConnection * connection,
const gchar *name, gpointer user_data)
{
GstClapperMpris *self = GST_CLAPPER_MPRIS (user_data);
GVariantBuilder builder;
g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self->base_skeleton),
connection, "/org/mpris/MediaPlayer2", NULL);
g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self->player_skeleton),
connection, "/org/mpris/MediaPlayer2", NULL);
if (self->identity)
gst_clapper_mpris_media_player2_set_identity (self->base_skeleton, self->identity);
if (self->desktop_entry)
gst_clapper_mpris_media_player2_set_desktop_entry (self->base_skeleton, self->desktop_entry);
_set_supported_uri_schemes (self);
gst_clapper_mpris_media_player2_player_set_playback_status (self->player_skeleton, "Stopped");
gst_clapper_mpris_media_player2_player_set_minimum_rate (self->player_skeleton, 0.01);
gst_clapper_mpris_media_player2_player_set_maximum_rate (self->player_skeleton, 2.0);
gst_clapper_mpris_media_player2_player_set_can_seek (self->player_skeleton, TRUE);
gst_clapper_mpris_media_player2_player_set_can_control (self->player_skeleton, TRUE);
g_object_bind_property (self->player_skeleton, "can-play",
self->player_skeleton, "can-pause", G_BINDING_DEFAULT);
g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY);
g_variant_builder_add (&builder, "{sv}", "mpris:trackid", g_variant_new_string (_get_mpris_trackid (self)));
g_variant_builder_add (&builder, "{sv}", "mpris:length", g_variant_new_uint64 (0));
if (self->default_art_url)
g_variant_builder_add (&builder, "{sv}", "mpris:artUrl", g_variant_new_string (self->default_art_url));
gst_clapper_mpris_media_player2_player_set_metadata (self->player_skeleton, g_variant_builder_end (&builder));
GST_DEBUG_OBJECT (self, "Ready");
}
static void
name_lost_cb (GDBusConnection * connection,
const gchar * name, gpointer user_data)
{
GstClapperMpris *self = GST_CLAPPER_MPRIS (user_data);
unregister (self);
}
static gboolean
mpris_update_props_dispatch (gpointer user_data)
{
GstClapperMpris *self = GST_CLAPPER_MPRIS (user_data);
GST_DEBUG_OBJECT (self, "Updating MPRIS props");
g_mutex_lock (&self->lock);
if (self->parse_media_info) {
GVariantBuilder builder;
guint64 duration;
const gchar *track_id, *uri, *title;
GST_DEBUG_OBJECT (self, "Parsing media info");
g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY);
track_id = _get_mpris_trackid (self);
uri = gst_clapper_media_info_get_uri (self->media_info);
title = gst_clapper_media_info_get_title (self->media_info);
if (track_id) {
g_variant_builder_add (&builder, "{sv}", "mpris:trackid",
g_variant_new_string (track_id));
GST_DEBUG_OBJECT (self, "mpris:trackid: %s", track_id);
}
if (uri) {
g_variant_builder_add (&builder, "{sv}", "xesam:url",
g_variant_new_string (uri));
GST_DEBUG_OBJECT (self, "xesam:url: %s", uri);
}
if (title) {
g_variant_builder_add (&builder, "{sv}", "xesam:title",
g_variant_new_string (title));
GST_DEBUG_OBJECT (self, "xesam:title: %s", title);
}
duration = gst_clapper_media_info_get_duration (self->media_info);
duration = (duration != GST_CLOCK_TIME_NONE) ? duration / GST_USECOND : 0;
g_variant_builder_add (&builder, "{sv}", "mpris:length", g_variant_new_uint64 (duration));
GST_DEBUG_OBJECT (self, "mpris:length: %ld", duration);
/* TODO: Check for image sample */
if (self->default_art_url) {
g_variant_builder_add (&builder, "{sv}", "mpris:artUrl", g_variant_new_string (self->default_art_url));
GST_DEBUG_OBJECT (self, "mpris:artUrl: %s", self->default_art_url);
}
GST_DEBUG_OBJECT (self, "Media info parsed");
self->parse_media_info = FALSE;
gst_clapper_mpris_media_player2_player_set_metadata (
self->player_skeleton, g_variant_builder_end (&builder));
}
if (gst_clapper_mpris_media_player2_player_get_can_play (
self->player_skeleton) != self->can_play) {
/* "can-play" is bound with "can-pause" */
gst_clapper_mpris_media_player2_player_set_can_play (
self->player_skeleton, self->can_play);
GST_DEBUG_OBJECT (self, "CanPlay/CanPause: %s", self->can_play ? "yes" : "no");
}
if (strcmp (gst_clapper_mpris_media_player2_player_get_playback_status (
self->player_skeleton), self->playback_status) != 0) {
gst_clapper_mpris_media_player2_player_set_playback_status (
self->player_skeleton, self->playback_status);
GST_DEBUG_OBJECT (self, "PlaybackStatus: %s", self->playback_status);
}
if (gst_clapper_mpris_media_player2_player_get_position (
self->player_skeleton) != self->position) {
gst_clapper_mpris_media_player2_player_set_position (
self->player_skeleton, self->position);
GST_DEBUG_OBJECT (self, "Position: %ld", self->position);
}
g_mutex_unlock (&self->lock);
GST_DEBUG_OBJECT (self, "MPRIS props updated");
return G_SOURCE_REMOVE;
}
static void
mpris_dispatcher_update_dispatch (GstClapperMpris * self)
{
if (!self->name_id)
return;
GST_DEBUG_OBJECT (self, "Queued update props dispatch");
g_main_context_invoke_full (self->context,
G_PRIORITY_DEFAULT, mpris_update_props_dispatch,
g_object_ref (self), g_object_unref);
}
static gpointer
gst_clapper_mpris_main (gpointer data)
{
GstClapperMpris *self = GST_CLAPPER_MPRIS (data);
GDBusConnectionFlags flags;
GDBusConnection *connection;
GSource *source;
gchar *address;
GST_TRACE_OBJECT (self, "Starting main thread");
g_main_context_push_thread_default (self->context);
source = g_idle_source_new ();
g_source_set_callback (source, (GSourceFunc) main_loop_running_cb, self,
NULL);
g_source_attach (source, self->context);
g_source_unref (source);
address = g_dbus_address_get_for_bus_sync (G_BUS_TYPE_SESSION, NULL, NULL);
if (!address) {
GST_WARNING_OBJECT (self, "No MPRIS bus address");
goto no_mpris;
}
GST_DEBUG_OBJECT (self, "Obtained MPRIS DBus address");
flags = G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT |
G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION;
connection = g_dbus_connection_new_for_address_sync (address,
flags, NULL, NULL, NULL);
g_free (address);
if (!connection) {
GST_WARNING_OBJECT (self, "No MPRIS bus connection");
goto no_mpris;
}
GST_DEBUG_OBJECT (self, "Obtained MPRIS DBus connection");
self->name_id = g_bus_own_name_on_connection (connection, self->own_name,
G_BUS_NAME_OWNER_FLAGS_NONE,
(GBusNameAcquiredCallback) name_acquired_cb,
(GBusNameLostCallback) name_lost_cb,
self, NULL);
g_object_unref (connection);
goto done;
no_mpris:
g_warning ("GstClapperMpris: failed to create DBus connection");
done:
GST_TRACE_OBJECT (self, "Starting main loop");
g_main_loop_run (self->loop);
GST_TRACE_OBJECT (self, "Stopped main loop");
unregister (self);
g_main_context_pop_thread_default (self->context);
GST_TRACE_OBJECT (self, "Stopped main thread");
return NULL;
}
void
gst_clapper_mpris_set_clapper (GstClapperMpris * self, GstClapper * clapper,
GstClapperSignalDispatcher * signal_dispatcher)
{
if (signal_dispatcher)
self->signal_dispatcher = g_object_ref (signal_dispatcher);
g_signal_connect (self->player_skeleton, "handle-play",
G_CALLBACK (handle_play_cb), clapper);
g_signal_connect (self->player_skeleton, "handle-pause",
G_CALLBACK (handle_pause_cb), clapper);
g_signal_connect (self->player_skeleton, "handle-play-pause",
G_CALLBACK (handle_play_pause_cb), clapper);
g_signal_connect (self->player_skeleton, "handle-seek",
G_CALLBACK (handle_seek_cb), clapper);
g_signal_connect (self->player_skeleton, "handle-set-position",
G_CALLBACK (handle_set_position_cb), clapper);
g_signal_connect (self->player_skeleton, "handle-open-uri",
G_CALLBACK (handle_open_uri_cb), clapper);
g_object_bind_property (clapper, "volume", self, "volume", G_BINDING_BIDIRECTIONAL);
g_signal_connect (self->player_skeleton, "notify::volume",
G_CALLBACK (handle_volume_notify_cb), self);
}
void
gst_clapper_mpris_set_playback_status (GstClapperMpris * self, const gchar * status)
{
g_mutex_lock (&self->lock);
if (strcmp (self->playback_status, status) == 0) {
g_mutex_unlock (&self->lock);
return;
}
g_free (self->playback_status);
self->playback_status = g_strdup (status);
self->can_play = strcmp (status, "Stopped") != 0;
g_mutex_unlock (&self->lock);
mpris_dispatcher_update_dispatch (self);
}
void
gst_clapper_mpris_set_position (GstClapperMpris * self, gint64 position)
{
position /= GST_USECOND;
g_mutex_lock (&self->lock);
if (self->position == position) {
g_mutex_unlock (&self->lock);
return;
}
self->position = position;
g_mutex_unlock (&self->lock);
mpris_dispatcher_update_dispatch (self);
}
void
gst_clapper_mpris_set_media_info (GstClapperMpris *self, GstClapperMediaInfo *info)
{
g_mutex_lock (&self->lock);
if (self->media_info)
g_object_unref (self->media_info);
self->media_info = info;
self->parse_media_info = TRUE;
g_mutex_unlock (&self->lock);
mpris_dispatcher_update_dispatch (self);
}
/**
* gst_clapper_mpris_new:
* @own_name: DBus own name
* @id_path: DBus id path used for prefix
* @identity: (allow-none): friendly name
* @desktop_entry: (allow-none): Desktop entry filename
* @default_art_url: (allow-none): filepath to default art
*
* Creates a new #GstClapperMpris instance.
*
* Returns: (transfer full): a new #GstClapperMpris instance
*/
GstClapperMpris *
gst_clapper_mpris_new (const gchar * own_name, const gchar * id_path,
const gchar * identity, const gchar * desktop_entry,
const gchar * default_art_url)
{
return g_object_new (GST_TYPE_CLAPPER_MPRIS,
"own-name", own_name, "id_path", id_path,
"identity", identity, "desktop-entry", desktop_entry,
"default-art-url", default_art_url, NULL);
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2021 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_MPRIS_H__
#define __GST_CLAPPER_MPRIS_H__
#include <glib.h>
#include <gio/gio.h>
#include <gst/clapper/clapper-prelude.h>
G_BEGIN_DECLS
typedef struct _GstClapperMpris GstClapperMpris;
typedef struct _GstClapperMprisClass GstClapperMprisClass;
#define GST_TYPE_CLAPPER_MPRIS (gst_clapper_mpris_get_type ())
#define GST_IS_CLAPPER_MPRIS(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_CLAPPER_MPRIS))
#define GST_IS_CLAPPER_MPRIS_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_CLAPPER_MPRIS))
#define GST_CLAPPER_MPRIS_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_CLAPPER_MPRIS, GstClapperMprisClass))
#define GST_CLAPPER_MPRIS(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_CLAPPER_MPRIS, GstClapperMpris))
#define GST_CLAPPER_MPRIS_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_CLAPPER_MPRIS, GstClapperMprisClass))
#define GST_CLAPPER_MPRIS_CAST(obj) ((GstClapperMpris*)(obj))
#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC
G_DEFINE_AUTOPTR_CLEANUP_FUNC(GstClapperMpris, g_object_unref)
#endif
GST_CLAPPER_API
GType gst_clapper_mpris_get_type (void);
GST_CLAPPER_API
GstClapperMpris * gst_clapper_mpris_new (const gchar *own_name, const gchar *id_path, const gchar *identity,
const gchar *desktop_entry, const gchar *default_art_url);
G_END_DECLS
#endif /* __GST_CLAPPER_MPRIS_H__ */

View File

@@ -46,10 +46,12 @@
#include "gstclapper-signal-dispatcher-private.h"
#include "gstclapper-video-renderer-private.h"
#include "gstclapper-media-info-private.h"
#include "gstclapper-mpris-private.h"
GST_DEBUG_CATEGORY_STATIC (gst_clapper_debug);
#define GST_CAT_DEFAULT gst_clapper_debug
#define DEFAULT_STATE GST_CLAPPER_STATE_STOPPED
#define DEFAULT_URI NULL
#define DEFAULT_POSITION GST_CLOCK_TIME_NONE
#define DEFAULT_DURATION GST_CLOCK_TIME_NONE
@@ -75,6 +77,8 @@ enum
PROP_0,
PROP_VIDEO_RENDERER,
PROP_SIGNAL_DISPATCHER,
PROP_MPRIS,
PROP_STATE,
PROP_URI,
PROP_SUBURI,
PROP_POSITION,
@@ -106,6 +110,7 @@ enum
SIGNAL_ERROR,
SIGNAL_WARNING,
SIGNAL_VIDEO_DIMENSIONS_CHANGED,
SIGNAL_MEDIA_INFO_UPDATED,
SIGNAL_MUTE_CHANGED,
SIGNAL_LAST
};
@@ -124,6 +129,7 @@ struct _GstClapper
GstClapperVideoRenderer *video_renderer;
GstClapperSignalDispatcher *signal_dispatcher;
GstClapperMpris *mpris;
gchar *uri;
gchar *redirect_uri;
@@ -138,7 +144,7 @@ struct _GstClapper
GstElement *playbin;
GstBus *bus;
GstState target_state, current_state;
gboolean is_live, is_eos;
gboolean is_live;
GSource *tick_source, *ready_timeout_source;
GstClockTime cached_duration;
@@ -168,6 +174,13 @@ struct _GstClapper
* is emitted after gst_clapper_stop/pause() has been called by the user. */
gboolean inhibit_sigs;
/* If TRUE, player is in initial ready state after
* new media was loaded and it can be played */
gboolean can_start;
/* If should emit media info updated signal */
gboolean needs_info_update;
/* For playbin3 */
gboolean use_playbin3;
GstStreamCollection *collection;
@@ -263,6 +276,9 @@ 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;
self->can_start = FALSE;
self->app_state = GST_CLAPPER_STATE_STOPPED;
GST_TRACE_OBJECT (self, "Initialized");
}
@@ -292,6 +308,18 @@ gst_clapper_class_init (GstClapperClass * klass)
G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY |
G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
param_specs[PROP_MPRIS] =
g_param_spec_object ("mpris",
"MPRIS", "Clapper MPRIS for playback control over DBus",
GST_TYPE_CLAPPER_MPRIS,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
param_specs[PROP_STATE] =
g_param_spec_enum ("state", "Clapper State", "Current player state",
GST_TYPE_CLAPPER_STATE, DEFAULT_STATE, G_PARAM_READABLE |
G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
param_specs[PROP_URI] = g_param_spec_string ("uri", "URI", "Current URI",
DEFAULT_URI, G_PARAM_READWRITE |
G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
@@ -421,6 +449,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,
@@ -468,7 +501,7 @@ gst_clapper_finalize (GObject * object)
{
GstClapper *self = GST_CLAPPER (object);
GST_TRACE_OBJECT (self, "Finalizing");
GST_TRACE_OBJECT (self, "Finalize");
g_free (self->uri);
g_free (self->redirect_uri);
@@ -484,6 +517,8 @@ gst_clapper_finalize (GObject * object)
g_object_unref (self->video_renderer);
if (self->signal_dispatcher)
g_object_unref (self->signal_dispatcher);
if (self->mpris)
g_object_unref (self->mpris);
if (self->current_vis_element)
gst_object_unref (self->current_vis_element);
if (self->collection)
@@ -540,10 +575,10 @@ gst_clapper_set_uri_internal (gpointer user_data)
gst_clapper_stop_internal (self, FALSE);
g_mutex_lock (&self->lock);
GST_DEBUG_OBJECT (self, "Changing URI to '%s'", GST_STR_NULL (self->uri));
g_object_set (self->playbin, "uri", self->uri, NULL);
g_object_set (self->playbin, "suburi", NULL, NULL);
self->can_start = TRUE;
if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID,
signals[SIGNAL_URI_LOADED], 0, NULL, NULL, NULL) != 0) {
@@ -556,8 +591,6 @@ gst_clapper_set_uri_internal (gpointer user_data)
(GDestroyNotify) uri_loaded_signal_data_free);
}
g_object_set (self->playbin, "suburi", NULL, NULL);
g_mutex_unlock (&self->lock);
return G_SOURCE_REMOVE;
@@ -628,6 +661,9 @@ gst_clapper_set_property (GObject * object, guint prop_id,
case PROP_SIGNAL_DISPATCHER:
self->signal_dispatcher = g_value_dup_object (value);
break;
case PROP_MPRIS:
self->mpris = g_value_dup_object (value);
break;
case PROP_URI:{
g_mutex_lock (&self->lock);
g_free (self->uri);
@@ -720,6 +756,16 @@ gst_clapper_get_property (GObject * object, guint prop_id,
GstClapper *self = GST_CLAPPER (object);
switch (prop_id) {
case PROP_MPRIS:
g_mutex_lock (&self->lock);
g_value_set_object (value, self->mpris);
g_mutex_unlock (&self->lock);
break;
case PROP_STATE:
g_mutex_lock (&self->lock);
g_value_set_enum (value, self->app_state);
g_mutex_unlock (&self->lock);
break;
case PROP_URI:
g_mutex_lock (&self->lock);
g_value_set_string (value, self->uri);
@@ -838,6 +884,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);
self->needs_info_update = FALSE;
data->clapper = g_object_ref (self);
g_mutex_lock (&self->lock);
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;
@@ -893,9 +982,12 @@ change_state (GstClapper * self, GstClapperState state)
gst_clapper_state_get_name (state));
self->app_state = state;
if (state == GST_CLAPPER_STATE_STOPPED && self->rate != 1.0) {
self->rate = 1.0;
emit_rate_notify (self);
if (state == GST_CLAPPER_STATE_STOPPED) {
self->needs_info_update = FALSE;
if (self->rate != 1.0) {
self->rate = 1.0;
emit_rate_notify (self);
}
}
if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID,
@@ -908,6 +1000,23 @@ change_state (GstClapper * self, GstClapperState state)
state_changed_dispatch, data,
(GDestroyNotify) state_changed_signal_data_free);
}
if (!self->mpris)
return;
switch (state) {
case GST_CLAPPER_STATE_STOPPED:
gst_clapper_mpris_set_playback_status (self->mpris, "Stopped");
break;
case GST_CLAPPER_STATE_PAUSED:
gst_clapper_mpris_set_playback_status (self->mpris, "Paused");
break;
case GST_CLAPPER_STATE_PLAYING:
gst_clapper_mpris_set_playback_status (self->mpris, "Playing");
break;
default:
break;
}
}
typedef struct
@@ -959,6 +1068,8 @@ tick_cb (gpointer user_data)
position_updated_dispatch, data,
(GDestroyNotify) position_updated_signal_data_free);
}
if (self->mpris)
gst_clapper_mpris_set_position (self->mpris, position);
}
return G_SOURCE_CONTINUE;
@@ -1073,7 +1184,6 @@ emit_error (GstClapper * self, GError * err)
self->target_state = GST_STATE_NULL;
self->current_state = GST_STATE_NULL;
self->is_live = FALSE;
self->is_eos = FALSE;
gst_element_set_state (self->playbin, GST_STATE_NULL);
change_state (self, GST_CLAPPER_STATE_STOPPED);
self->buffering = 100;
@@ -1255,14 +1365,13 @@ eos_cb (G_GNUC_UNUSED GstBus * bus, G_GNUC_UNUSED GstMessage * msg,
tick_cb (self);
remove_tick_source (self);
/* When connected client should handle what to do (stop/repeat) */
if (g_signal_handler_find (self, G_SIGNAL_MATCH_ID,
signals[SIGNAL_END_OF_STREAM], 0, NULL, NULL, NULL) != 0) {
gst_clapper_signal_dispatcher_dispatch (self->signal_dispatcher, self,
eos_dispatch, g_object_ref (self), (GDestroyNotify) g_object_unref);
}
change_state (self, GST_CLAPPER_STATE_STOPPED);
self->buffering = 100;
self->is_eos = TRUE;
} else
gst_clapper_stop_internal (self, FALSE);
}
typedef struct
@@ -1468,7 +1577,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
@@ -1501,7 +1617,8 @@ duration_changed_signal_data_free (DurationChangedSignalData * data)
static void
emit_duration_changed (GstClapper * self, GstClockTime duration)
{
if (self->cached_duration == duration)
if (self->cached_duration == duration
|| self->cached_duration / (250 * GST_MSECOND) == duration / (250 * GST_MSECOND))
return;
GST_DEBUG_OBJECT (self, "Duration changed %" GST_TIME_FORMAT,
@@ -1568,6 +1685,16 @@ state_changed_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg,
} else {
self->cached_duration = GST_CLOCK_TIME_NONE;
}
emit_media_info_updated (self);
if (self->mpris) {
GstClapperMediaInfo *info;
g_mutex_lock (&self->lock);
info = gst_clapper_media_info_copy (self->media_info);
g_mutex_unlock (&self->lock);
gst_clapper_mpris_set_media_info (self->mpris, info);
}
}
if (new_state == GST_STATE_PAUSED
@@ -1679,8 +1806,12 @@ request_state_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg,
static void
media_info_update (GstClapper * self, GstClapperMediaInfo * info)
{
g_free (info->title);
info->title = get_from_tags (self, info, get_title);
/* Update title from new tags or leave the title from URI */
gchar *tags_title = get_from_tags (self, info, get_title);
if (tags_title) {
g_free (info->title);
info->title = tags_title;
}
g_free (info->container);
info->container = get_from_tags (self, info, get_container_format);
@@ -1812,6 +1943,27 @@ element_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, gpointer user_data)
}
}
static void
qos_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg, gpointer user_data)
{
GstClapper *self = GST_CLAPPER (user_data);
gboolean live;
guint64 running_time, stream_time, timestamp, duration;
gst_message_parse_qos (msg, &live, &running_time, &stream_time,
&timestamp, &duration);
GST_DEBUG_OBJECT (self, "QOS dropped buffer"
", element live: %s"
", running time: %" GST_TIME_FORMAT
", stream time: %" GST_TIME_FORMAT
", timestamp: %" GST_TIME_FORMAT
", duration: %" GST_TIME_FORMAT,
live ? "yes" : "no", GST_TIME_ARGS (running_time),
GST_TIME_ARGS (stream_time), GST_TIME_ARGS (timestamp),
GST_TIME_ARGS (duration));
}
/* Must be called with lock */
static gboolean
update_stream_collection (GstClapper * self, GstStreamCollection * collection)
@@ -1911,6 +2063,16 @@ streams_selected_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg,
g_mutex_unlock (&self->lock);
}
static gboolean
clapper_get_has_flag (GstClapper * self, gint pos)
{
gint flags;
g_object_get (self->playbin, "flags", &flags, NULL);
return (flags & pos) == pos;
}
static void
clapper_set_flag (GstClapper * self, gint pos)
{
@@ -1965,11 +2127,14 @@ gst_clapper_subtitle_info_update (GstClapper * self,
{
GstClapperSubtitleInfo *info = (GstClapperSubtitleInfo *) stream_info;
if (stream_info->tags) {
/* Free the old info */
g_free (info->title);
info->title = NULL;
g_free (info->language);
info->language = NULL;
/* free the old language info */
g_free (info->language);
info->language = NULL;
if (stream_info->tags) {
gst_tag_list_get_string (stream_info->tags, GST_TAG_TITLE, &info->title);
/* First try to get the language full name from tag, if name is not
* available then try language code. If we find the language code
@@ -2011,13 +2176,10 @@ gst_clapper_subtitle_info_update (GstClapper * self,
g_free (suburi);
}
}
} else {
g_free (info->language);
info->language = NULL;
}
GST_DEBUG_OBJECT (self, "language=%s", info->language);
GST_DEBUG_OBJECT (self, "Subtitle title: %s", info->title);
GST_DEBUG_OBJECT (self, "Subtitle language: %s", info->language);
}
static void
@@ -2209,19 +2371,6 @@ gst_clapper_stream_info_find_from_stream_id (GstClapperMediaInfo * media_info,
return NULL;
}
static gboolean
is_track_enabled (GstClapper * self, gint pos)
{
gint flags;
g_object_get (G_OBJECT (self->playbin), "flags", &flags, NULL);
if ((flags & pos))
return TRUE;
return FALSE;
}
static GstClapperStreamInfo *
gst_clapper_stream_info_get_current (GstClapper * self, const gchar * prop,
GType type)
@@ -2269,6 +2418,7 @@ stream_notify_cb (GstStreamCollection * collection, GstStream * stream,
{
GstClapperStreamInfo *info;
const gchar *stream_id;
gboolean emit_update = FALSE;
if (!self->media_info)
return;
@@ -2281,9 +2431,14 @@ stream_notify_cb (GstStreamCollection * collection, GstStream * stream,
g_mutex_lock (&self->lock);
info =
gst_clapper_stream_info_find_from_stream_id (self->media_info, stream_id);
if (info)
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
@@ -2523,6 +2678,32 @@ subtitle_changed_cb (G_GNUC_UNUSED GObject * object, gpointer user_data)
g_mutex_unlock (&self->lock);
}
static gchar *
get_title_from_uri (const gchar * uri)
{
gchar *proto = gst_uri_get_protocol (uri);
gchar *title = NULL;
if (strcmp (proto, "file") == 0) {
const gchar *ext = strrchr (uri, '.');
if (ext && strlen (ext) < 8) {
gchar *filename = g_filename_from_uri (uri, NULL, NULL);
if (filename) {
gchar *base = g_path_get_basename (filename);
g_free (filename);
title = g_strndup (base, strlen (base) - strlen (ext));
g_free (base);
}
}
} else if (strcmp (proto, "dvb") == 0) {
const gchar *channel = strrchr (uri, '/') + 1;
title = g_strdup (channel);
}
g_free (proto);
return title;
}
static void *
get_title (GstTagList * tags)
{
@@ -2639,12 +2820,15 @@ gst_clapper_media_info_create (GstClapper * self)
}
media_info->title = get_from_tags (self, media_info, get_title);
if (!media_info->title)
media_info->title = get_title_from_uri (self->uri);
media_info->container =
get_from_tags (self, media_info, get_container_format);
media_info->image_sample = get_from_tags (self, media_info, get_cover_sample);
GST_DEBUG_OBJECT (self, "uri: %s title: %s duration: %" GST_TIME_FORMAT
" seekable: %s live: %s container: %s image_sample %p",
GST_DEBUG_OBJECT (self, "uri: %s, title: %s, duration: %" GST_TIME_FORMAT
", seekable: %s, live: %s, container: %s, image_sample %p",
media_info->uri, media_info->title, GST_TIME_ARGS (media_info->duration),
media_info->seekable ? "yes" : "no", media_info->is_live ? "yes" : "no",
media_info->container, media_info->image_sample);
@@ -2672,8 +2856,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 +2930,30 @@ 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)
{
GstElementFactory *factory;
GParamSpec *prop;
factory = gst_element_get_factory (element);
if (factory) {
gchar *plugin_name = gst_object_get_name (GST_OBJECT_CAST (factory));
if (plugin_name) {
GST_INFO_OBJECT (self, "Plugin setup: %s", plugin_name);
/* TODO: Set plugin props */
}
g_free (plugin_name);
}
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
@@ -2787,12 +2997,23 @@ gst_clapper_main (gpointer data)
GstElement *video_sink =
gst_clapper_video_renderer_create_video_sink (self->video_renderer, self);
if (video_sink) {
const gchar *fps_env;
GstPad *video_sink_pad = gst_element_get_static_pad (video_sink, "sink");
if (video_sink_pad) {
g_signal_connect (video_sink_pad, "notify::caps",
(GCallback) notify_caps_cb, self);
gst_object_unref (video_sink_pad);
}
fps_env = g_getenv ("GST_CLAPPER_DISPLAY_FPS");
if (fps_env && g_str_has_prefix (fps_env, "1")) {
GstElement *fpsdisplaysink =
gst_element_factory_make ("fpsdisplaysink", "fpsdisplaysink");
if (fpsdisplaysink) {
GST_DEBUG_OBJECT (self, "FPS display enabled");
g_object_set (fpsdisplaysink, "video-sink", video_sink, NULL);
video_sink = fpsdisplaysink;
}
}
g_object_set (self->playbin, "video-sink", video_sink, NULL);
}
}
@@ -2808,6 +3029,9 @@ gst_clapper_main (gpointer data)
self->bus = bus = gst_element_get_bus (self->playbin);
gst_bus_add_signal_watch (bus);
if (self->mpris)
gst_clapper_mpris_set_clapper (self->mpris, self, self->signal_dispatcher);
g_signal_connect (G_OBJECT (bus), "message::error", G_CALLBACK (error_cb),
self);
g_signal_connect (G_OBJECT (bus), "message::warning", G_CALLBACK (warning_cb),
@@ -2830,6 +3054,9 @@ gst_clapper_main (gpointer data)
g_signal_connect (G_OBJECT (bus), "message::tag", G_CALLBACK (tags_cb), self);
g_signal_connect (G_OBJECT (bus), "message::toc", G_CALLBACK (toc_cb), self);
if (gst_debug_category_get_threshold (gst_clapper_debug) >= GST_LEVEL_DEBUG)
g_signal_connect (G_OBJECT (bus), "message::qos", G_CALLBACK (qos_cb), self);
if (self->use_playbin3) {
g_signal_connect (G_OBJECT (bus), "message::stream-collection",
G_CALLBACK (stream_collection_cb), self);
@@ -2855,14 +3082,13 @@ 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;
change_state (self, GST_CLAPPER_STATE_STOPPED);
self->buffering = 100;
self->is_eos = FALSE;
self->is_live = FALSE;
self->rate = 1.0;
self->seek_mode = DEFAULT_SEEK_MODE;
@@ -2906,6 +3132,7 @@ gst_clapper_main (gpointer data)
* gst_clapper_new:
* @video_renderer: (transfer full) (allow-none): GstClapperVideoRenderer to use
* @signal_dispatcher: (transfer full) (allow-none): GstClapperSignalDispatcher to use
* @mpris: (transfer full) (allow-none): GstClapperMpris to use
*
* Creates a new #GstClapper instance that uses @signal_dispatcher to dispatch
* signals to some event loop system, or emits signals directly if NULL is
@@ -2919,18 +3146,20 @@ gst_clapper_main (gpointer data)
*/
GstClapper *
gst_clapper_new (GstClapperVideoRenderer * video_renderer,
GstClapperSignalDispatcher * signal_dispatcher)
GstClapperSignalDispatcher * signal_dispatcher,
GstClapperMpris * mpris)
{
GstClapper *self;
self =
g_object_new (GST_TYPE_CLAPPER, "video-renderer", video_renderer,
"signal-dispatcher", signal_dispatcher, NULL);
self = g_object_new (GST_TYPE_CLAPPER, "video-renderer", video_renderer,
"signal-dispatcher", signal_dispatcher, "mpris", mpris, NULL);
if (video_renderer)
g_object_unref (video_renderer);
if (signal_dispatcher)
g_object_unref (signal_dispatcher);
if (mpris)
g_object_unref (mpris);
return self;
}
@@ -2956,7 +3185,7 @@ gst_clapper_play_internal (gpointer user_data)
if (self->current_state < GST_STATE_PAUSED)
change_state (self, GST_CLAPPER_STATE_BUFFERING);
if (self->current_state >= GST_STATE_PAUSED && !self->is_eos
if (self->current_state >= GST_STATE_PAUSED
&& self->buffering >= 100 && !(self->seek_position != GST_CLOCK_TIME_NONE
|| self->seek_pending)) {
state_ret = gst_element_set_state (self->playbin, GST_STATE_PLAYING);
@@ -2973,21 +3202,6 @@ gst_clapper_play_internal (gpointer user_data)
GST_DEBUG_OBJECT (self, "Pipeline is live");
}
if (self->is_eos) {
gboolean ret;
GST_DEBUG_OBJECT (self, "Was EOS, seeking to beginning");
self->is_eos = FALSE;
ret =
gst_element_seek_simple (self->playbin, GST_FORMAT_TIME,
GST_SEEK_FLAG_FLUSH, 0);
if (!ret) {
GST_ERROR_OBJECT (self, "Seek to beginning failed");
gst_clapper_stop_internal (self, TRUE);
gst_clapper_play_internal (self);
}
}
return G_SOURCE_REMOVE;
}
@@ -3002,8 +3216,14 @@ gst_clapper_play (GstClapper * self)
{
g_return_if_fail (GST_IS_CLAPPER (self));
if (!self->can_start && self->app_state == GST_CLAPPER_STATE_STOPPED) {
GST_DEBUG_OBJECT (self, "Player stopped, play request ignored");
return;
}
g_mutex_lock (&self->lock);
self->inhibit_sigs = FALSE;
self->can_start = FALSE;
g_mutex_unlock (&self->lock);
g_main_context_invoke_full (self->context, G_PRIORITY_DEFAULT,
@@ -3044,21 +3264,6 @@ gst_clapper_pause_internal (gpointer user_data)
GST_DEBUG_OBJECT (self, "Pipeline is live");
}
if (self->is_eos) {
gboolean ret;
GST_DEBUG_OBJECT (self, "Was EOS, seeking to beginning");
self->is_eos = FALSE;
ret =
gst_element_seek_simple (self->playbin, GST_FORMAT_TIME,
GST_SEEK_FLAG_FLUSH, 0);
if (!ret) {
GST_ERROR_OBJECT (self, "Seek to beginning failed");
gst_clapper_stop_internal (self, TRUE);
gst_clapper_pause_internal (self);
}
}
return G_SOURCE_REMOVE;
}
@@ -3073,15 +3278,40 @@ gst_clapper_pause (GstClapper * self)
{
g_return_if_fail (GST_IS_CLAPPER (self));
/* Do not try to pause on DVD navigation */
if (G_LIKELY (self->cached_duration > 1000000000)) {
g_mutex_lock (&self->lock);
self->inhibit_sigs = FALSE;
g_mutex_unlock (&self->lock);
g_main_context_invoke_full (self->context, G_PRIORITY_DEFAULT,
gst_clapper_pause_internal, self, NULL);
if (self->app_state == GST_CLAPPER_STATE_STOPPED) {
GST_DEBUG_OBJECT (self, "Player stopped, pause request ignored");
return;
}
if (G_UNLIKELY (self->cached_duration <= GST_SECOND)) {
GST_DEBUG_OBJECT (self, "Cannot pause on this stream");
return;
}
g_mutex_lock (&self->lock);
self->inhibit_sigs = FALSE;
g_mutex_unlock (&self->lock);
g_main_context_invoke_full (self->context, G_PRIORITY_DEFAULT,
gst_clapper_pause_internal, self, NULL);
}
/**
* gst_clapper_toggle_play:
* @clapper: #GstClapper instance
*
* Toggle between play and pause on the loaded stream.
* This function does nothing if player is stopped.
*/
void
gst_clapper_toggle_play (GstClapper * self)
{
g_return_if_fail (GST_IS_CLAPPER (self));
if (self->app_state == GST_CLAPPER_STATE_PLAYING)
gst_clapper_pause (self);
else
gst_clapper_play (self);
}
static void
@@ -3102,14 +3332,11 @@ gst_clapper_stop_internal (GstClapper * self, gboolean transient)
self->target_state = GST_STATE_NULL;
self->current_state = GST_STATE_READY;
self->is_live = FALSE;
self->is_eos = FALSE;
gst_bus_set_flushing (self->bus, TRUE);
gst_element_set_state (self->playbin, GST_STATE_READY);
gst_bus_set_flushing (self->bus, FALSE);
change_state (self, transient
&& self->app_state !=
GST_CLAPPER_STATE_STOPPED ? GST_CLAPPER_STATE_BUFFERING :
GST_CLAPPER_STATE_STOPPED);
change_state (self, transient && self->app_state != GST_CLAPPER_STATE_STOPPED
? GST_CLAPPER_STATE_BUFFERING : GST_CLAPPER_STATE_STOPPED);
self->buffering = 100;
self->cached_duration = GST_CLOCK_TIME_NONE;
g_mutex_lock (&self->lock);
@@ -3213,7 +3440,6 @@ gst_clapper_seek_internal_locked (GstClapper * self)
g_mutex_unlock (&self->lock);
remove_tick_source (self);
self->is_eos = FALSE;
flags |= GST_SEEK_FLAG_FLUSH;
@@ -3353,6 +3579,29 @@ gst_clapper_seek (GstClapper * self, GstClockTime position)
g_mutex_unlock (&self->lock);
}
/**
* gst_clapper_seek_offset:
* @clapper: #GstClapper instance
* @offset: offset from current position to seek to in nanoseconds
*
* Seeks the currently-playing stream to the @offset time
* in nanoseconds.
*/
void
gst_clapper_seek_offset (GstClapper * self, GstClockTime offset)
{
GstClockTime position;
g_return_if_fail (GST_IS_CLAPPER (self));
g_return_if_fail (GST_CLOCK_TIME_IS_VALID (offset));
position = gst_clapper_get_position (self);
/* TODO: Prevent negative values */
gst_clapper_seek (self, position + offset);
}
static void
remove_seek_source (GstClapper * self)
{
@@ -3364,6 +3613,24 @@ remove_seek_source (GstClapper * self)
self->seek_source = NULL;
}
/**
* gst_clapper_get_state:
* @clapper: #GstClapper instance
*
* Returns: Current player state
*/
GstClapperState
gst_clapper_get_state (GstClapper * self)
{
GstClapperState state;
g_return_val_if_fail (GST_IS_CLAPPER (self), DEFAULT_STATE);
g_object_get (self, "state", &state, NULL);
return state;
}
/**
* gst_clapper_get_uri:
* @clapper: #GstClapper instance
@@ -3567,6 +3834,28 @@ gst_clapper_get_pipeline (GstClapper * self)
return val;
}
/**
* gst_clapper_get_mpris:
* @clapper: #GstClapper instance
*
* A Function to get the #GstClapperMpris instance.
*
* Returns: (transfer full): mpris instance.
*
* The caller should free it with g_object_unref()
*/
GstClapperMpris *
gst_clapper_get_mpris (GstClapper * self)
{
GstClapperMpris *val;
g_return_val_if_fail (GST_IS_CLAPPER (self), NULL);
g_object_get (self, "mpris", &val, NULL);
return val;
}
/**
* gst_clapper_get_media_info:
* @clapper: #GstClapper instance
@@ -3611,7 +3900,7 @@ gst_clapper_get_current_audio_track (GstClapper * self)
g_return_val_if_fail (GST_IS_CLAPPER (self), NULL);
if (!is_track_enabled (self, GST_PLAY_FLAG_AUDIO))
if (!clapper_get_has_flag (self, GST_PLAY_FLAG_AUDIO))
return NULL;
if (self->use_playbin3) {
@@ -3643,7 +3932,7 @@ gst_clapper_get_current_video_track (GstClapper * self)
g_return_val_if_fail (GST_IS_CLAPPER (self), NULL);
if (!is_track_enabled (self, GST_PLAY_FLAG_VIDEO))
if (!clapper_get_has_flag (self, GST_PLAY_FLAG_VIDEO))
return NULL;
if (self->use_playbin3) {
@@ -3675,7 +3964,7 @@ gst_clapper_get_current_subtitle_track (GstClapper * self)
g_return_val_if_fail (GST_IS_CLAPPER (self), NULL);
if (!is_track_enabled (self, GST_PLAY_FLAG_SUBTITLE))
if (!clapper_get_has_flag (self, GST_PLAY_FLAG_SUBTITLE))
return NULL;
if (self->use_playbin3) {
@@ -3732,7 +4021,7 @@ gst_clapper_set_audio_track (GstClapper * self, gint stream_index)
GstClapperStreamInfo *info;
gboolean ret = TRUE;
g_return_val_if_fail (GST_IS_CLAPPER (self), 0);
g_return_val_if_fail (GST_IS_CLAPPER (self), FALSE);
g_mutex_lock (&self->lock);
info = gst_clapper_stream_info_find (self->media_info,
@@ -3773,7 +4062,7 @@ gst_clapper_set_video_track (GstClapper * self, gint stream_index)
GstClapperStreamInfo *info;
gboolean ret = TRUE;
g_return_val_if_fail (GST_IS_CLAPPER (self), 0);
g_return_val_if_fail (GST_IS_CLAPPER (self), FALSE);
/* check if stream_index exist in our internal media_info list */
g_mutex_lock (&self->lock);
@@ -3815,7 +4104,7 @@ gst_clapper_set_subtitle_track (GstClapper * self, gint stream_index)
GstClapperStreamInfo *info;
gboolean ret = TRUE;
g_return_val_if_fail (GST_IS_CLAPPER (self), 0);
g_return_val_if_fail (GST_IS_CLAPPER (self), FALSE);
g_mutex_lock (&self->lock);
info = gst_clapper_stream_info_find (self->media_info,
@@ -3954,7 +4243,7 @@ gst_clapper_get_current_visualization (GstClapper * self)
g_return_val_if_fail (GST_IS_CLAPPER (self), NULL);
if (!is_track_enabled (self, GST_PLAY_FLAG_VIS))
if (!clapper_get_has_flag (self, GST_PLAY_FLAG_VIS))
return NULL;
g_object_get (self->playbin, "vis-plugin", &vis_plugin, NULL);

View File

@@ -30,6 +30,7 @@
#include <gst/clapper/gstclapper-signal-dispatcher.h>
#include <gst/clapper/gstclapper-video-renderer.h>
#include <gst/clapper/gstclapper-media-info.h>
#include <gst/clapper/gstclapper-mpris.h>
G_BEGIN_DECLS
@@ -153,7 +154,8 @@ GST_CLAPPER_API
GType gst_clapper_get_type (void);
GST_CLAPPER_API
GstClapper * gst_clapper_new (GstClapperVideoRenderer *video_renderer, GstClapperSignalDispatcher *signal_dispatcher);
GstClapper * gst_clapper_new (GstClapperVideoRenderer *video_renderer, GstClapperSignalDispatcher *signal_dispatcher,
GstClapperMpris *mpris);
GST_CLAPPER_API
void gst_clapper_play (GstClapper *clapper);
@@ -161,12 +163,22 @@ void gst_clapper_play (GstClapper *clapper
GST_CLAPPER_API
void gst_clapper_pause (GstClapper *clapper);
GST_CLAPPER_API
void gst_clapper_toggle_play (GstClapper *clapper);
GST_CLAPPER_API
void gst_clapper_stop (GstClapper *clapper);
GST_CLAPPER_API
void gst_clapper_seek (GstClapper *clapper, GstClockTime position);
GST_CLAPPER_API
void gst_clapper_seek_offset (GstClapper *clapper, GstClockTime offset);
GST_CLAPPER_API
GstClapperState
gst_clapper_get_state (GstClapper *clapper);
GST_CLAPPER_API
GstClapperSeekMode
gst_clapper_get_seek_mode (GstClapper *clapper);
@@ -213,6 +225,10 @@ void gst_clapper_set_mute (GstClapper *clapper
GST_CLAPPER_API
GstElement * gst_clapper_get_pipeline (GstClapper *clapper);
GST_CLAPPER_API
GstClapperMpris *
gst_clapper_get_mpris (GstClapper *clapper);
GST_CLAPPER_API
void gst_clapper_set_video_track_enabled (GstClapper *clapper, gboolean enabled);

View File

@@ -0,0 +1,745 @@
/*
* 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
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 GstFlowReturn gst_clapper_gl_sink_wait_event (GstBaseSink * bsink, GstEvent * event);
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);
#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;
gobject_class->finalize = gst_clapper_gl_sink_finalize;
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));
gst_gtk_install_shared_properties (gobject_class);
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;
gstbasesink_class->wait_event = gst_clapper_gl_sink_wait_event;
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->keep_last_frame = DEFAULT_KEEP_LAST_FRAME;
clapper_sink->had_eos = FALSE;
}
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 ();
g_object_bind_property (clapper_sink, "force-aspect-ratio", clapper_sink->widget,
"force-aspect-ratio", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
g_object_bind_property (clapper_sink, "pixel-aspect-ratio", clapper_sink->widget,
"pixel-aspect-ratio", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
g_object_bind_property (clapper_sink, "keep-last-frame", clapper_sink->widget,
"keep-last-frame", 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_KEEP_LAST_FRAME:
g_value_set_boolean (value, clapper_sink->keep_last_frame);
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_KEEP_LAST_FRAME:
clapper_sink->keep_last_frame = 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_NULL_TO_READY:
GST_OBJECT_LOCK (clapper_sink);
clapper_sink->had_eos = FALSE;
if (clapper_sink->widget) {
GTK_CLAPPER_GL_WIDGET_LOCK (clapper_sink->widget);
clapper_sink->widget->ignore_buffers = FALSE;
GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_sink->widget);
}
GST_OBJECT_UNLOCK (clapper_sink);
break;
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_READY_TO_NULL:
GST_OBJECT_LOCK (clapper_sink);
if (clapper_sink->widget) {
GTK_CLAPPER_GL_WIDGET_LOCK (clapper_sink->widget);
clapper_sink->widget->ignore_buffers =
!clapper_sink->had_eos || !clapper_sink->keep_last_frame;
GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_sink->widget);
}
GST_OBJECT_UNLOCK (clapper_sink);
/* Fall through to render black bg */
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_wait_event (GstBaseSink * bsink, GstEvent * event)
{
GstClapperGLSink *clapper_sink = GST_CLAPPER_GL_SINK (bsink);
GstFlowReturn ret;
ret = GST_BASE_SINK_CLASS (parent_class)->wait_event (bsink, event);
switch (event->type) {
case GST_EVENT_EOS:
if (ret == GST_FLOW_OK) {
GST_OBJECT_LOCK (clapper_sink);
clapper_sink->had_eos = TRUE;
GST_OBJECT_UNLOCK (clapper_sink);
}
break;
default:
break;
}
return ret;
}
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,105 @@
/*
* 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;
gboolean had_eos;
/* properties */
gboolean force_aspect_ratio;
gint par_n, par_d;
gboolean keep_last_frame;
gboolean 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__ */

View File

@@ -19,6 +19,7 @@
* Boston, MA 02110-1301, USA.
*/
#include <gst/gst.h>
#include "gstgtkutils.h"
struct invoke_context
@@ -69,3 +70,26 @@ gst_gtk_invoke_on_main (GThreadFunc func, gpointer data)
return info.res;
}
void
gst_gtk_install_shared_properties (GObjectClass *gobject_class)
{
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_KEEP_LAST_FRAME,
g_param_spec_boolean ("keep-last-frame",
"Keep last frame",
"Keep showing last video frame after playback instead of black screen",
DEFAULT_KEEP_LAST_FRAME,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
}

View File

@@ -22,8 +22,25 @@
#ifndef __GST_GTK_UTILS_H__
#define __GST_GTK_UTILS_H__
#define DEFAULT_FORCE_ASPECT_RATIO TRUE
#define DEFAULT_PAR_N 0
#define DEFAULT_PAR_D 1
#define DEFAULT_KEEP_LAST_FRAME FALSE
#include <glib.h>
#include <glib-object.h>
enum
{
PROP_0,
PROP_WIDGET,
PROP_FORCE_ASPECT_RATIO,
PROP_PIXEL_ASPECT_RATIO,
PROP_KEEP_LAST_FRAME
};
gpointer gst_gtk_invoke_on_main (GThreadFunc func, gpointer data);
void gst_gtk_install_shared_properties (GObjectClass *gobject_class);
#endif /* __GST_GTK_UTILS_H__ */

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,112 @@
/*
* 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 keep_last_frame;
gint display_width;
gint display_height;
/* Widget dimensions */
gint scaled_width;
gint scaled_height;
/* Position coords */
gdouble last_pos_x;
gdouble last_pos_y;
gboolean negotiated;
gboolean ignore_buffers;
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

@@ -1,3 +1,5 @@
gnome = import('gnome')
gstclapper_sources = [
'gstclapper.c',
'gstclapper-signal-dispatcher.c',
@@ -6,13 +8,12 @@ gstclapper_sources = [
'gstclapper-g-main-context-signal-dispatcher.c',
'gstclapper-video-overlay-video-renderer.c',
'gstclapper-visualization.c',
'gstclapper-mpris.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',
@@ -25,6 +26,7 @@ gstclapper_headers = [
'gstclapper-g-main-context-signal-dispatcher.h',
'gstclapper-video-overlay-video-renderer.h',
'gstclapper-visualization.h',
'gstclapper-mpris.h',
'gstclapper-gtk4-plugin.h',
]
gstclapper_defines = [
@@ -32,7 +34,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
@@ -43,10 +44,13 @@ if not gtk4_dep.version().version_compare('>=4.0.0')
error('GTK4 version on this system is too old')
endif
if gst_gl_have_window_x11 and gst_gl_have_platform_glx
if gst_gl_have_window_x11 and (gst_gl_have_platform_egl or gst_gl_have_platform_glx)
gtk_x11_dep = dependency('gtk4-x11', required : false)
if gtk_x11_dep.found()
gtk_deps += [gtk_x11_dep, gstglx11_dep]
gtk_deps += gtk_x11_dep
if gst_gl_have_platform_glx
gtk_deps += gstglx11_dep
endif
have_gtk_gl_windowing = true
endif
endif
@@ -54,24 +58,35 @@ endif
if gst_gl_have_window_wayland and gst_gl_have_platform_egl
gtk_wayland_dep = dependency('gtk4-wayland', required : false)
if gtk_wayland_dep.found()
gtk_deps += [gtk_wayland_dep, gstglegl_dep, gstglwayland_dep]
gtk_deps += [gtk_wayland_dep, gstglwayland_dep]
have_gtk_gl_windowing = true
endif
endif
if gst_gl_have_platform_egl
gtk_deps += gstglegl_dep
endif
if not have_gtk_gl_windowing
error('GTK4 widget requires GL windowing')
endif
gstclapper_mpris_gdbus = gnome.gdbus_codegen('gstclapper-mpris-gdbus',
sources: '../../../data/gstclapper-mpris-gdbus.xml',
interface_prefix: 'org.mpris.',
namespace: 'GstClapperMpris'
)
gstclapper = library('gstclapper-' + api_version,
gstclapper_sources,
gstclapper_sources + gstclapper_mpris_gdbus,
c_args : gstclapper_defines,
link_args : noseh_link_args,
include_directories : [configinc, libsinc],
version : libversion,
install : true,
install_dir : clapper_libdir,
dependencies : [gtk4_dep, gstbase_dep, gstvideo_dep, gstaudio_dep,
dependencies : [gtk4_dep, glib_dep, gio_dep,
gstbase_dep, gstvideo_dep, gstaudio_dep,
gsttag_dep, gstpbutils_dep, libm] + gtk_deps,
)

View File

@@ -1,5 +1,5 @@
project('com.github.rafostar.Clapper', 'c', 'cpp',
version: '0.1.0',
version: '0.3.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.3.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.3.0) unstable; urgency=low
* New version
-- Rafostar <rafostar.github@gmail.com> Fri, 26 Feb 2021 09:39:00 +0100
-- Rafostar <rafostar.github@gmail.com> Fri, 18 Jun 2021 09:39:00 +0100

View File

@@ -1,4 +1,6 @@
build/
builddir/
repo/
.flatpak-builder/
com.github.rafostar.Clapper.flatpak
flathub/com.github.rafostar.Clapper.json

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": [
@@ -13,39 +13,40 @@
"--share=network",
"--device=all",
"--filesystem=xdg-videos",
"--own-name=org.mpris.MediaPlayer2.Clapper",
"--talk-name=org.gnome.Shell",
"--env=GST_PLUGIN_SYSTEM_PATH=/app/lib/gstreamer-1.0",
"--env=GST_VAAPI_ALL_DRIVERS=1"
],
"modules": [
"lib/glib-networking.json",
"shared-modules/gudev/gudev.json",
"lib/pango.json",
"lib/libsass.json",
"lib/sassc.json",
"lib/gtk4.json",
"lib/liba52.json",
"lib/libmpeg2.json",
"lib/libdvdcss.json",
"lib/libdvdread.json",
"lib/libdvdnav.json",
"lib/libass.json",
"lib/ffmpeg.json",
"lib/uchardet.json",
"gstreamer-1.0/gstreamer.json",
"gstreamer-1.0/gst-plugins-base.json",
"gstreamer-1.0/gst-plugins-good.json",
"gstreamer-1.0/gst-plugins-bad.json",
"gstreamer-1.0/gst-plugins-ugly.json",
"gstreamer-1.0/gst-libav.json",
"gstreamer-1.0/gstreamer-vaapi.json",
"flathub/lib/glib-networking.json",
"flathub/shared-modules/gudev/gudev.json",
"flathub/lib/pango.json",
"flathub/lib/libsass.json",
"flathub/lib/sassc.json",
"flathub/lib/gtk4.json",
"flathub/lib/liba52.json",
"flathub/lib/libmpeg2.json",
"flathub/lib/libdvdcss.json",
"flathub/lib/libdvdread.json",
"flathub/lib/libdvdnav.json",
"flathub/lib/libass.json",
"flathub/lib/ffmpeg.json",
"flathub/lib/uchardet.json",
"flathub/gstreamer-1.0/gstreamer.json",
"flathub/gstreamer-1.0/gst-plugins-base.json",
"flathub/gstreamer-1.0/gst-plugins-good.json",
"flathub/gstreamer-1.0/gst-plugins-bad.json",
"flathub/gstreamer-1.0/gst-plugins-ugly.json",
"flathub/gstreamer-1.0/gst-libav.json",
"flathub/gstreamer-1.0/gstreamer-vaapi.json",
{
"name": "clapper",
"buildsystem": "meson",
"sources": [
{
"type": "git",
"url": "https://github.com/Rafostar/clapper.git"
"type": "dir",
"path": "../../."
}
]
}

1
pkgs/flatpak/flathub Submodule

Submodule pkgs/flatpak/flathub added at 2a3ed05245

View File

@@ -1,30 +0,0 @@
From 2c371f17af1695bd42f572d5ccdb837152b8b67a Mon Sep 17 00:00:00 2001
From: Thomas Coldrick <othko97@gmail.com>
Date: Thu, 8 Nov 2018 17:46:53 +0000
Subject: [PATCH] gst-libav-stop-caching-codecs
---
ext/libav/gstav.c | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/ext/libav/gstav.c b/ext/libav/gstav.c
index 2a88230..bfd19a1 100644
--- a/ext/libav/gstav.c
+++ b/ext/libav/gstav.c
@@ -155,6 +155,13 @@ plugin_init (GstPlugin * plugin)
/* build global ffmpeg param/property info */
gst_ffmpeg_cfg_init ();
+ gst_plugin_add_dependency_simple (plugin, NULL,
+ "/app/lib",
+ "libavcodec.so.58,"
+ "libavformat.so.58,"
+ "libswscale.so.5",
+ GST_PLUGIN_DEPENDENCY_FLAG_NONE);
+
gst_ffmpegaudenc_register (plugin);
gst_ffmpegvidenc_register (plugin);
gst_ffmpegauddec_register (plugin);
--
2.19.1

View File

@@ -1,20 +0,0 @@
{
"name": "gst-libav",
"buildsystem": "meson",
"config-opts": [
"-Ddoc=disabled",
"-Dtests=disabled"
],
"sources": [
{
"type": "git",
"url": "https://gitlab.freedesktop.org/gstreamer/gst-libav.git",
"tag": "1.18.1",
"commit": "097313530cae4a49437a779a9ded0ade8113c26b"
},
{
"type": "patch",
"path": "gst-libav-stop-caching-codecs.patch"
}
]
}

View File

@@ -1,89 +0,0 @@
From ab9ceccc8b7f0591f580abfa6901d27c49812a94 Mon Sep 17 00:00:00 2001
From: Rafostar <40623528+Rafostar@users.noreply.github.com>
Date: Sun, 10 Jan 2021 20:22:43 +0100
Subject: [PATCH 1/2] assrender: fix mimetype detection
Previously gst_structure_has_name was used to get a string to compare with supported mimetypes.
This is incorrect as above function returns a user defined structure name which is
not the structure mimetype value.
---
ext/assrender/gstassrender.c | 21 ++++++++++++---------
1 file changed, 12 insertions(+), 9 deletions(-)
diff --git a/ext/assrender/gstassrender.c b/ext/assrender/gstassrender.c
index e6d31985b..a69d3fe78 100644
--- a/ext/assrender/gstassrender.c
+++ b/ext/assrender/gstassrender.c
@@ -1557,7 +1557,7 @@ gst_ass_render_handle_tag_sample (GstAssRender * render, GstSample * sample)
const GstStructure *structure;
gboolean valid_mimetype, valid_extension;
guint i;
- const gchar *filename;
+ const gchar *mimetype, *filename;
buf = gst_sample_get_buffer (sample);
structure = gst_sample_get_info (sample);
@@ -1565,20 +1565,23 @@ gst_ass_render_handle_tag_sample (GstAssRender * render, GstSample * sample)
if (!buf || !structure)
return;
+ filename = gst_structure_get_string (structure, "filename");
+ if (!filename)
+ return;
+
valid_mimetype = FALSE;
valid_extension = FALSE;
- for (i = 0; i < G_N_ELEMENTS (mimetypes); i++) {
- if (gst_structure_has_name (structure, mimetypes[i])) {
- valid_mimetype = TRUE;
- break;
+ mimetype = gst_structure_get_string (structure, "mimetype");
+ if (mimetype) {
+ for (i = 0; i < G_N_ELEMENTS (mimetypes); i++) {
+ if (strcmp (mimetype, mimetypes[i]) == 0) {
+ valid_mimetype = TRUE;
+ break;
+ }
}
}
- filename = gst_structure_get_string (structure, "filename");
- if (!filename)
- return;
-
if (!valid_mimetype) {
guint len = strlen (filename);
const gchar *extension = filename + len - 4;
--
2.28.0
From fd7d46171b2abcd3ac247491f01a91444e7b95b2 Mon Sep 17 00:00:00 2001
From: Rafostar <40623528+Rafostar@users.noreply.github.com>
Date: Sun, 10 Jan 2021 20:26:58 +0100
Subject: [PATCH 2/2] assrender: add "vnd.ms-opentype" to supported mimetypes
The "application/vnd.ms-opentype" mimetype is commonly used mimetype
for fonts with .otf extension, handle it without checking the file extension.
---
ext/assrender/gstassrender.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/ext/assrender/gstassrender.c b/ext/assrender/gstassrender.c
index a69d3fe78..96b062c50 100644
--- a/ext/assrender/gstassrender.c
+++ b/ext/assrender/gstassrender.c
@@ -1546,7 +1546,8 @@ gst_ass_render_handle_tag_sample (GstAssRender * render, GstSample * sample)
static const gchar *mimetypes[] = {
"application/x-font-ttf",
"application/x-font-otf",
- "application/x-truetype-font"
+ "application/x-truetype-font",
+ "application/vnd.ms-opentype"
};
static const gchar *extensions[] = {
".otf",
--
2.28.0

View File

@@ -1,30 +0,0 @@
From 1c8538d8f8c2181106d626d67784af6db094036e Mon Sep 17 00:00:00 2001
From: Rafostar <rafostar.github@gmail.com>
Date: Thu, 19 Nov 2020 18:03:11 +0100
Subject: [PATCH] assrender: fix smooth scaling by disabling hinting
When ass hinting value is set to anything other than NONE,
subtitles cannot use smooth scaling, thus all animations will jitter.
The libass author warns about possibility of breaking some scripts when it is enabled,
so lets do what is recommended and disable it to get the smooth scaling working.
---
ext/assrender/gstassrender.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ext/assrender/gstassrender.c b/ext/assrender/gstassrender.c
index e99458bf29..111987b9d8 100644
--- a/ext/assrender/gstassrender.c
+++ b/ext/assrender/gstassrender.c
@@ -916,7 +916,7 @@ gst_ass_render_negotiate (GstAssRender * render, GstCaps * caps)
ass_set_pixel_aspect (render->ass_renderer,
(gdouble) render->info.par_n / (gdouble) render->info.par_d);
ass_set_font_scale (render->ass_renderer, 1.0);
- ass_set_hinting (render->ass_renderer, ASS_HINTING_LIGHT);
+ ass_set_hinting (render->ass_renderer, ASS_HINTING_NONE);
ass_set_fonts (render->ass_renderer, "Arial", "sans-serif", 1, NULL, 1);
ass_set_fonts (render->ass_renderer, NULL, "Sans", 1, NULL, 1);
--
GitLab

View File

@@ -1,75 +0,0 @@
From f9af93d841546ca7898350ae14ed57448b24a644 Mon Sep 17 00:00:00 2001
From: Seungha Yang <seungha@centricular.com>
Date: Sat, 14 Nov 2020 03:16:07 +0900
Subject: [PATCH 1/2] codecs: h264decoder: Don't give up to decode due to
missing reference picture
Missing reference picture is very common thing for broken/malformed stream.
Decoder should be able to keep decoding if it's not a very critical error.
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/-/merge_requests/1809>
---
gst-libs/gst/codecs/gsth264decoder.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/gst-libs/gst/codecs/gsth264decoder.c b/gst-libs/gst/codecs/gsth264decoder.c
index e6d20af208..40446d92df 100644
--- a/gst-libs/gst/codecs/gsth264decoder.c
+++ b/gst-libs/gst/codecs/gsth264decoder.c
@@ -2354,7 +2354,7 @@ modify_ref_pic_list (GstH264Decoder * self, int list)
if (!pic) {
GST_WARNING_OBJECT (self, "Malformed stream, no pic num %d",
pic_num_lx);
- return FALSE;
+ break;
}
shift_right_and_insert (ref_pic_listx, ref_idx_lx,
num_ref_idx_lX_active_minus1, pic);
@@ -2380,7 +2380,7 @@ modify_ref_pic_list (GstH264Decoder * self, int list)
if (!pic) {
GST_WARNING_OBJECT (self, "Malformed stream, no pic num %d",
list_mod->value.long_term_pic_num);
- return FALSE;
+ break;
}
shift_right_and_insert (ref_pic_listx, ref_idx_lx,
num_ref_idx_lX_active_minus1, pic);
--
GitLab
From 9011a58491b089461762a8f550892de434af5c29 Mon Sep 17 00:00:00 2001
From: Seungha Yang <seungha@centricular.com>
Date: Sat, 14 Nov 2020 03:20:19 +0900
Subject: [PATCH 2/2] vah264dec: Allow missing reference picture
baseclass might provide reference picture list with null picture.
Ensure picture before filling picture information.
Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/-/merge_requests/1809>
---
sys/va/gstvah264dec.c | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/sys/va/gstvah264dec.c b/sys/va/gstvah264dec.c
index e90f84bb44..184af430fa 100644
--- a/sys/va/gstvah264dec.c
+++ b/sys/va/gstvah264dec.c
@@ -198,7 +198,13 @@ _fill_ref_pic_list (VAPictureH264 va_reflist[32], GArray * reflist)
for (i = 0; i < reflist->len; i++) {
GstH264Picture *picture = g_array_index (reflist, GstH264Picture *, i);
- _fill_vaapi_pic (&va_reflist[i], picture);
+
+ if (picture) {
+ _fill_vaapi_pic (&va_reflist[i], picture);
+ } else {
+ /* list might include null picture if reference picture was missing */
+ _init_vaapi_pic (&va_reflist[i]);
+ }
}
for (; i < 32; i++)
--
GitLab

View File

@@ -1,41 +0,0 @@
{
"name": "gst-plugins-bad",
"buildsystem": "meson",
"config-opts": [
"-Ddoc=disabled",
"-Dexamples=disabled",
"-Dtests=disabled",
"-Dnls=disabled",
"-Dgobject-cast-checks=disabled",
"-Dglib-asserts=disabled",
"-Dglib-checks=disabled",
"-Dextra-checks=disabled",
"-Dvulkan=disabled",
"-Dwebrtc=disabled",
"-Dwasapi=disabled",
"-Dwasapi2=disabled",
"-Dwinks=disabled",
"-Dwinscreencap=disabled"
],
"sources": [
{
"type": "git",
"url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad.git",
"tag": "1.18.1",
"commit": "e5c3c106a2da607953fea36e3a253b382c939684"
},
{
"type": "patch",
"path": "gst-plugins-bad-vah264dec-fix-seeking-errors.patch"
},
{
"type": "patch",
"path": "gst-plugins-bad-assrender-smooth-scaling.patch"
},
{
"type": "patch",
"path": "gst-plugins-bad-assrender-fix-mimetype-detection.patch"
}
]
}

View File

@@ -1,142 +0,0 @@
From 61a66babede5a587783a1d4eb28e950a755ff362 Mon Sep 17 00:00:00 2001
From: Rafostar <rafostar.github@gmail.com>
Date: Wed, 25 Nov 2020 14:44:21 +0100
Subject: [PATCH] subparse: Autodetect subtitle text encoding
Use "uchardet" to guess the subtitle text encoding if it is not in UTF-8
or manually specified instead of blindly guessing its "ISO-8859-15".
The "uchardet" dependency is optional and when is not available at
compile time, then old behaviour will be used.
---
gst/subparse/gstsubparse.c | 58 +++++++++++++++++++++++++++++++++-----
gst/subparse/meson.build | 12 ++++++--
2 files changed, 61 insertions(+), 9 deletions(-)
diff --git a/gst/subparse/gstsubparse.c b/gst/subparse/gstsubparse.c
index 382e430f2..42283d2d1 100644
--- a/gst/subparse/gstsubparse.c
+++ b/gst/subparse/gstsubparse.c
@@ -31,6 +31,10 @@
#include <sys/types.h>
#include <glib.h>
+#if defined(HAVE_UCHARDET)
+#include <uchardet.h>
+#endif
+
#include "gstsubparse.h"
#include "gstssaparse.h"
#include "samiparse.h"
@@ -148,8 +152,9 @@ gst_sub_parse_class_init (GstSubParseClass * klass)
"Encoding to assume if input subtitles are not in UTF-8 or any other "
"Unicode encoding. If not set, the GST_SUBTITLE_ENCODING environment "
"variable will be checked for an encoding to use. If that is not set "
- "either, ISO-8859-15 will be assumed.", DEFAULT_ENCODING,
- G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+ "either, then if plugin was build with uchardet support it will be "
+ "used to guess the encoding, otherwise ISO-8859-15 will be assumed.",
+ DEFAULT_ENCODING, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class, PROP_VIDEOFPS,
gst_param_spec_fraction ("video-fps", "Video framerate",
@@ -439,6 +444,35 @@ detect_encoding (const gchar * str, gsize len)
return NULL;
}
+static gchar *
+uchardet_detect_encoding (const gchar * str, gsize len)
+{
+ gchar *charset = NULL;
+ gint retval;
+
+#if defined(HAVE_UCHARDET)
+ uchardet_t handle = uchardet_new ();
+ retval = uchardet_handle_data (handle, str, len);
+
+ GST_DEBUG ("detecting encoding with uchardet using %li characters", len);
+
+ if (retval != 0) {
+ GST_WARNING ("could not handle data with uchardet");
+ } else {
+ uchardet_data_end (handle);
+ charset = g_strdup (uchardet_get_charset (handle));
+
+ if (charset == NULL || *charset == '\0')
+ GST_WARNING ("uchardet could not detect encoding");
+ else
+ GST_INFO ("uchardet detected encoding: %s", charset);
+ }
+ uchardet_delete (handle);
+#endif
+
+ return charset;
+}
+
static gchar *
convert_encoding (GstSubParse * self, const gchar * str, gsize len,
gsize * consumed)
@@ -481,11 +515,18 @@ convert_encoding (GstSubParse * self, const gchar * str, gsize len,
encoding = g_getenv ("GST_SUBTITLE_ENCODING");
}
if (encoding == NULL || *encoding == '\0') {
- /* if local encoding is UTF-8 and no encoding specified
- * via the environment variable, assume ISO-8859-15 */
- if (g_get_charset (&encoding)) {
+ /* no encoding specified via the environment variable either,
+ * so try to autodetect with uchardet */
+ encoding = uchardet_detect_encoding (str, len);
+ }
+
+ /* if uchardet failed and local encoding is UTF-8, assume ISO-8859-15 */
+ if (encoding == NULL || *encoding == '\0') {
+ if (g_get_charset (&encoding))
encoding = "ISO-8859-15";
- }
+ } else {
+ /* reuse the detected encoding from now on */
+ self->detected_encoding = g_strdup (encoding);
}
ret = gst_convert_to_utf8 (str, len, encoding, consumed, &err);
@@ -2159,7 +2200,10 @@ gst_subparse_type_find (GstTypeFind * tf, gpointer private)
enc = g_getenv ("GST_SUBTITLE_ENCODING");
if (enc == NULL || *enc == '\0') {
/* if local encoding is UTF-8 and no encoding specified
- * via the environment variable, assume ISO-8859-15 */
+ * via the environment variable, assume ISO-8859-15
+ *
+ * Encoding here is only used for type find, so no need
+ * to run through uchardet at this point */
if (g_get_charset (&enc)) {
enc = "ISO-8859-15";
}
diff --git a/gst/subparse/meson.build b/gst/subparse/meson.build
index 9a76601f0..2dcf8830f 100644
--- a/gst/subparse/meson.build
+++ b/gst/subparse/meson.build
@@ -6,12 +6,20 @@ subparse_sources = [
'mpl2parse.c',
'qttextparse.c',
]
+subparse_defines = []
+subparse_optional_deps = []
+
+subparse_uchardet_dep = dependency('uchardet', required : false)
+if subparse_uchardet_dep.found()
+ subparse_defines += '-DHAVE_UCHARDET'
+ subparse_optional_deps += subparse_uchardet_dep
+endif
gstsubparse = library('gstsubparse',
subparse_sources,
- c_args : gst_plugins_base_args,
+ c_args : gst_plugins_base_args + subparse_defines,
include_directories: [configinc, libsinc],
- dependencies : [gst_base_dep],
+ dependencies : [gst_base_dep] + subparse_optional_deps,
install : true,
install_dir : plugins_install_dir,
)
--
2.26.2

View File

@@ -1,30 +0,0 @@
{
"name": "gst-plugins-base",
"buildsystem": "meson",
"config-opts": [
"--wrap-mode=nofallback",
"-Ddoc=disabled",
"-Dexamples=disabled",
"-Dtests=disabled",
"-Dnls=disabled",
"-Dgobject-cast-checks=disabled",
"-Dglib-asserts=disabled",
"-Dglib-checks=disabled",
"-Dgl_api=opengl,gles2",
"-Dgl_platform=egl,glx"
],
"sources": [
{
"type": "git",
"url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-base.git",
"tag": "1.18.1",
"commit": "4013b8003e78971dd01b055066c12f8aaadb8897"
},
{
"type": "patch",
"path": "gst-plugins-base-autodetect-subtitle-text-encoding.patch"
}
]
}

View File

@@ -1,67 +0,0 @@
From b2ad7c68c3478c433a0ede4aed6afb2f0b32702c Mon Sep 17 00:00:00 2001
From: Rafostar <40623528+Rafostar@users.noreply.github.com>
Date: Sun, 10 Jan 2021 15:44:45 +0100
Subject: [PATCH] matroska: fix attachments detection in large data blocks
Due to max block size limit being set to 15MB, large
attachments (fonts of few MB in size) were undetected
as attachments consist of single data block. Raise max
data block limit to 30MB to fix that.
---
gst/matroska/matroska-demux.c | 34 ++++++++++++++++------------------
1 file changed, 16 insertions(+), 18 deletions(-)
diff --git a/gst/matroska/matroska-demux.c b/gst/matroska/matroska-demux.c
index 4d0234743..ce906e5a3 100644
--- a/gst/matroska/matroska-demux.c
+++ b/gst/matroska/matroska-demux.c
@@ -5115,30 +5115,28 @@ gst_matroska_demux_parse_contents (GstMatroskaDemux * demux, GstEbmlRead * ebml)
}
#define GST_FLOW_OVERFLOW GST_FLOW_CUSTOM_ERROR
-
-#define MAX_BLOCK_SIZE (15 * 1024 * 1024)
+#define MAX_BLOCK_SIZE (60 * 1024 * 1024)
static inline GstFlowReturn
gst_matroska_demux_check_read_size (GstMatroskaDemux * demux, guint64 bytes)
{
- if (G_UNLIKELY (bytes > MAX_BLOCK_SIZE)) {
- /* only a few blocks are expected/allowed to be large,
- * and will be recursed into, whereas others will be read and must fit */
- if (demux->streaming) {
- /* fatal in streaming case, as we can't step over easily */
- GST_ELEMENT_ERROR (demux, STREAM, DEMUX, (NULL),
- ("reading large block of size %" G_GUINT64_FORMAT " not supported; "
- "file might be corrupt.", bytes));
- return GST_FLOW_ERROR;
- } else {
- /* indicate higher level to quietly give up */
- GST_DEBUG_OBJECT (demux,
- "too large block of size %" G_GUINT64_FORMAT, bytes);
- return GST_FLOW_ERROR;
- }
- } else {
+ if (G_LIKELY (bytes <= MAX_BLOCK_SIZE))
return GST_FLOW_OK;
+
+ /* only a few blocks are expected/allowed to be large,
+ * and will be recursed into, whereas others will be read and must fit */
+ if (demux->streaming) {
+ /* fatal in streaming case, as we can't step over easily */
+ GST_ELEMENT_ERROR (demux, STREAM, DEMUX, (NULL),
+ ("reading large block of size %" G_GUINT64_FORMAT " not supported; "
+ "file might be corrupt.", bytes));
+ } else {
+ /* indicate higher level to quietly give up */
+ GST_DEBUG_OBJECT (demux, "too large block of size %" G_GUINT64_FORMAT,
+ bytes);
}
+
+ return GST_FLOW_ERROR;
}
/* returns TRUE if we truly are in error state, and should give up */
--
2.29.2

View File

@@ -1,36 +0,0 @@
From 4e5b2b0c3aeefffdd9613e33678cade25fac3fe4 Mon Sep 17 00:00:00 2001
From: Rafostar <rafostar.github@gmail.com>
Date: Sun, 10 Jan 2021 19:55:31 +0100
Subject: [PATCH] matroska: treat non-image structure as attachment and set
mimetype
Otherwise each structure is named as GstTagImageInfo even if it does not contain any images
which is misleading. Also set the structure mimetype to fix assrender fonts detection.
---
gst/matroska/matroska-read-common.c | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/gst/matroska/matroska-read-common.c b/gst/matroska/matroska-read-common.c
index 90d6e38e1..628e19669 100644
--- a/gst/matroska/matroska-read-common.c
+++ b/gst/matroska/matroska-read-common.c
@@ -851,10 +851,13 @@ gst_matroska_read_common_parse_attached_file (GstMatroskaReadCommon * common,
}
/* Set filename and description in the info */
- if (info == NULL)
- info = gst_structure_new_empty ("GstTagImageInfo");
-
+ if (info == NULL) {
+ const gchar *structure_name = (image_type != GST_TAG_IMAGE_TYPE_NONE) ?
+ "GstTagImageInfo" : "GstTagAttachmentInfo";
+ info = gst_structure_new_empty (structure_name);
+ }
gst_structure_set (info, "filename", G_TYPE_STRING, filename, NULL);
+ gst_structure_set (info, "mimetype", G_TYPE_STRING, mimetype, NULL);
if (description)
gst_structure_set (info, "description", G_TYPE_STRING, description, NULL);
--
2.28.0

View File

@@ -1,31 +0,0 @@
{
"name": "gst-plugins-good",
"buildsystem": "meson",
"config-opts": [
"-Ddoc=disabled",
"-Dexamples=disabled",
"-Dtests=disabled",
"-Dnls=disabled",
"-Dgobject-cast-checks=disabled",
"-Dglib-asserts=disabled",
"-Dglib-checks=disabled",
"-Dgtk3=disabled"
],
"sources": [
{
"type": "git",
"url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-good.git",
"tag": "1.18.1",
"commit": "7c44cdb0e00dd1c9932d8e5194b09fcf4e1e6fc1"
},
{
"type": "patch",
"path": "gst-plugins-good-matroska-fix-attachments-detection.patch"
},
{
"type": "patch",
"path": "gst-plugins-good-matroska-set-attachment-mimetype.patch"
}
]
}

View File

@@ -1,22 +0,0 @@
{
"name": "gst-plugins-ugly",
"buildsystem": "meson",
"config-opts": [
"-Ddoc=disabled",
"-Dnls=disabled",
"-Dtests=disabled",
"-Dgobject-cast-checks=disabled",
"-Dglib-asserts=disabled",
"-Dglib-checks=disabled",
"-Dmpeg2dec=enabled"
],
"sources": [
{
"type": "git",
"url": "https://gitlab.freedesktop.org/gstreamer/gst-plugins-ugly.git",
"tag": "1.18.1",
"commit": "720672eed30b3be47b2f26d67554786c0d3693ad"
}
]
}

View File

@@ -1,83 +0,0 @@
From 65fc08032a41ae8779d1845dce2c00b1efa2955c Mon Sep 17 00:00:00 2001
From: Rafostar <rafostar.github@gmail.com>
Date: Tue, 22 Dec 2020 15:08:21 +0100
Subject: [PATCH] glx: Iterate over FBConfig and select 8 bit color size
---
gst-libs/gst/vaapi/gstvaapiutils_glx.c | 40 ++++++++++++++++++++++++--
1 file changed, 38 insertions(+), 2 deletions(-)
diff --git a/gst-libs/gst/vaapi/gstvaapiutils_glx.c b/gst-libs/gst/vaapi/gstvaapiutils_glx.c
index ccd7832b..f73106c2 100644
--- a/gst-libs/gst/vaapi/gstvaapiutils_glx.c
+++ b/gst-libs/gst/vaapi/gstvaapiutils_glx.c
@@ -301,9 +301,17 @@ gl_create_context (Display * dpy, int screen, GLContextState * parent)
GLX_RED_SIZE, 8,
GLX_GREEN_SIZE, 8,
GLX_BLUE_SIZE, 8,
+ GLX_ALPHA_SIZE, 8,
None
};
+ const GLint rgba_colors[4] = {
+ GLX_RED_SIZE,
+ GLX_GREEN_SIZE,
+ GLX_BLUE_SIZE,
+ GLX_ALPHA_SIZE
+ };
+
cs = malloc (sizeof (*cs));
if (!cs)
goto error;
@@ -333,11 +341,38 @@ gl_create_context (Display * dpy, int screen, GLContextState * parent)
if (!fbconfigs)
goto error;
- /* Find out a GLXFBConfig compatible with the parent context */
+ /* Find out a 8 bit GLXFBConfig compatible with the parent context */
for (n = 0; n < n_fbconfigs; n++) {
+ gboolean sizes_correct = FALSE;
+ int cn;
+
status = glXGetFBConfigAttrib (parent->display,
fbconfigs[n], GLX_FBCONFIG_ID, &val);
- if (status == Success && val == fbconfig_id)
+ if (status != Success)
+ goto error;
+ if (val != fbconfig_id)
+ continue;
+
+ /* Iterate over RGBA sizes in fbconfig */
+ for (cn = 0; cn < 4; cn++) {
+ int size = 0;
+
+ status = glXGetFBConfigAttrib (parent->display,
+ fbconfigs[n], rgba_colors[cn], &size);
+ if (status != Success)
+ goto error;
+
+ /* Last check is for alpha
+ * and alpha is optional */
+ if (cn == 3) {
+ if (size == 0 || size == 8) {
+ sizes_correct = TRUE;
+ break;
+ }
+ } else if (size != 8)
+ break;
+ }
+ if (sizes_correct)
break;
}
if (n == n_fbconfigs)
@@ -809,6 +844,7 @@ gl_create_pixmap_object (Display * dpy, guint width, guint height)
GLX_RED_SIZE, 8,
GLX_GREEN_SIZE, 8,
GLX_BLUE_SIZE, 8,
+ GLX_ALPHA_SIZE, 8,
GL_NONE,
};
--
2.28.0

View File

@@ -1,21 +0,0 @@
{
"name": "gstreamer-vaapi",
"buildsystem": "meson",
"config-opts": [
"-Ddoc=disabled",
"-Dexamples=disabled",
"-Dtests=disabled"
],
"sources": [
{
"type": "git",
"url": "https://gitlab.freedesktop.org/gstreamer/gstreamer-vaapi.git",
"tag": "1.18.1",
"commit": "f9e925af3645439f7b7a4580700fcd6ce17fc1c9"
},
{
"type": "patch",
"path": "gstreamer-vaapi-glx-select-8-bit-color-size.patch"
}
]
}

View File

@@ -1,24 +0,0 @@
{
"name": "gstreamer",
"buildsystem": "meson",
"config-opts": [
"-Ddoc=disabled",
"-Dgtk_doc=disabled",
"-Dexamples=disabled",
"-Dtests=disabled",
"-Dbenchmarks=disabled",
"-Dnls=disabled",
"-Dgobject-cast-checks=disabled",
"-Dglib-asserts=disabled",
"-Dglib-checks=disabled",
"-Dextra-checks=disabled"
],
"sources": [
{
"type": "git",
"url": "https://gitlab.freedesktop.org/gstreamer/gstreamer.git",
"tag": "1.18.1",
"commit": "29a8099d1d4bd8717c13923e710e92e67e335353"
}
]
}

View File

@@ -1,22 +0,0 @@
--- a52dec-0.7.4/configure~ 2002-07-28 06:50:42.000000000 +0300
+++ a52dec-0.7.4/configure 2006-02-16 23:03:07.000000000 +0200
@@ -5839,7 +5839,7 @@
shlibpath_overrides_runpath=unknown
version_type=none
dynamic_linker="$host_os ld.so"
-sys_lib_dlsearch_path_spec="/lib /usr/lib"
+sys_lib_dlsearch_path_spec="/lib64 /usr/lib64 /lib /usr/lib"
sys_lib_search_path_spec="/lib /usr/lib /usr/local/lib"
case $host_os in
--- a52dec-0.7.4/aclocal.m4~ 2002-07-28 06:50:38.000000000 +0300
+++ a52dec-0.7.4/aclocal.m4 2006-02-16 23:02:38.000000000 +0200
@@ -2141,7 +2141,7 @@
shlibpath_overrides_runpath=unknown
version_type=none
dynamic_linker="$host_os ld.so"
-sys_lib_dlsearch_path_spec="/lib /usr/lib"
+sys_lib_dlsearch_path_spec="/lib64 /usr/lib64 /lib /usr/lib"
sys_lib_search_path_spec="/lib /usr/lib /usr/local/lib"
case $host_os in

View File

@@ -1,23 +0,0 @@
--- ./configure~ 2002-07-28 06:50:42.000000000 +0300
+++ ./configure 2003-04-13 17:20:53.000000000 +0300
@@ -2857,13 +2857,13 @@
case "$host" in
i?86-* | k?-*)
- case "$host" in
- i386-*) OPT_CFLAGS="$CFLAGS -mcpu=i386";;
- i486-*) OPT_CFLAGS="$CFLAGS -mcpu=i486";;
- i586-*) OPT_CFLAGS="$CFLAGS -mcpu=pentium";;
- i686-*) OPT_CFLAGS="$CFLAGS -mcpu=pentiumpro";;
- k6-*) OPT_CFLAGS="$CFLAGS -mcpu=k6";;
- esac
+# case "$host" in
+# i386-*) OPT_CFLAGS="$CFLAGS -mcpu=i386";;
+# i486-*) OPT_CFLAGS="$CFLAGS -mcpu=i486";;
+# i586-*) OPT_CFLAGS="$CFLAGS -mcpu=pentium";;
+# i686-*) OPT_CFLAGS="$CFLAGS -mcpu=pentiumpro";;
+# k6-*) OPT_CFLAGS="$CFLAGS -mcpu=k6";;
+# esac
echo "$as_me:$LINENO: checking if $CC supports $OPT_CFLAGS flags" >&5
echo $ECHO_N "checking if $CC supports $OPT_CFLAGS flags... $ECHO_C" >&6
SAVE_CFLAGS="$CFLAGS"

View File

@@ -1,29 +0,0 @@
{
"name": "ffmpeg",
"cleanup": [
"/lib/ffmpeg/examples"
],
"config-opts": [
"--disable-debug",
"--disable-doc",
"--disable-static",
"--disable-everything",
"--enable-gpl",
"--enable-version3",
"--enable-optimizations",
"--enable-runtime-cpudetect",
"--enable-shared",
"--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",
"--enable-demuxer=gif,yuv4mpegpipe"
],
"sources": [
{
"type": "git",
"url": "https://git.ffmpeg.org/ffmpeg.git",
"tag": "n4.3.1",
"commit": "6b6b9e593dd4d3aaf75f48d40a13ef03bdef9fdb"
}
]
}

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

@@ -1,31 +0,0 @@
From b413ee2c7d458c7005d3d3d1da8822cd86893ac0 Mon Sep 17 00:00:00 2001
From: Rafostar <40623528+Rafostar@users.noreply.github.com>
Date: Fri, 4 Dec 2020 19:25:34 +0100
Subject: [PATCH] popover: Call unrealize on hide
When popover is shown "realize" method is called which creates a new
surface for popup. Unfortunately this causes performance drop on Wayland until that
surface is destroyed what happens inside "unrealize" method during popover destruction.
This commit changes default behavior in a way that surface will be destroyed
when popover is closed and app will ragain the performance it lost when
popover was shown.
---
gtk/gtkpopover.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/gtk/gtkpopover.c b/gtk/gtkpopover.c
index 504dcd6cc1..a7a764d483 100644
--- a/gtk/gtkpopover.c
+++ b/gtk/gtkpopover.c
@@ -951,6 +951,7 @@ gtk_popover_hide (GtkWidget *widget)
gtk_popover_set_mnemonics_visible (GTK_POPOVER (widget), FALSE);
_gtk_widget_set_visible_flag (widget, FALSE);
+ gtk_widget_unrealize (widget);
gtk_widget_unmap (widget);
g_signal_emit (widget, signals[CLOSED], 0);
}
--
GitLab

View File

@@ -1,33 +0,0 @@
{
"name": "gtk",
"buildsystem": "meson",
"config-opts": [
"--wrap-mode=nofallback",
"-Dbroadway-backend=true",
"-Dwin32-backend=false",
"-Dmacos-backend=false",
"-Dmedia-ffmpeg=disabled",
"-Dprint-cups=disabled",
"-Dprint-cloudprint=disabled",
"-Dintrospection=enabled",
"-Ddemos=false",
"-Dbuild-examples=false",
"-Dbuild-tests=false"
],
"sources": [
{
"type": "git",
"url": "https://gitlab.gnome.org/GNOME/gtk.git",
"tag": "4.1.1",
"commit": "1f284fcd706de5b0b8c54fee3ff61880caf1d167"
},
{
"type": "patch",
"path": "gtk4-popover-unrealize.patch"
},
{
"type": "patch",
"path": "gtk4-broadway-fix-unsafe-variable-type.patch"
}
]
}

View File

@@ -1,39 +0,0 @@
From 4c18c43b4d4ccb1d05ae73b813f26ba193fbeee3 Mon Sep 17 00:00:00 2001
From: Bastien Nocera <hadess@hadess.net>
Date: Fri, 18 Jan 2019 17:37:13 +0100
Subject: [PATCH] Prefer PIC
---
configure | 2 +-
liba52/configure.incl | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/configure b/configure
index b81fdff..bc0267c 100755
--- a/configure
+++ b/configure
@@ -9640,7 +9640,7 @@ _ACEOF
-LIBA52_CFLAGS="$LIBA52_CFLAGS -prefer-non-pic"
+LIBA52_CFLAGS="$LIBA52_CFLAGS -prefer-pic"
# Check whether --enable-double or --disable-double was given.
if test "${enable_double+set}" = set; then
diff --git a/liba52/configure.incl b/liba52/configure.incl
index 4dbbcea..5eb69ee 100644
--- a/liba52/configure.incl
+++ b/liba52/configure.incl
@@ -2,7 +2,7 @@ AC_SUBST([LIBA52_CFLAGS])
AC_SUBST([LIBA52_LIBS])
dnl avoid -fPIC when possible
-LIBA52_CFLAGS="$LIBA52_CFLAGS -prefer-non-pic"
+LIBA52_CFLAGS="$LIBA52_CFLAGS -prefer-pic"
AC_ARG_ENABLE([double],
[ --enable-double use double-precision samples])
--
2.20.1

View File

@@ -1,17 +0,0 @@
diff -ru a52dec.orig/liba52/imdct.c a52dec/liba52/imdct.c
--- a52dec.orig/liba52/imdct.c 2012-02-06 19:40:21.000000000 +0200
+++ a52dec/liba52/imdct.c 2012-02-06 19:40:53.000000000 +0200
@@ -419,13 +419,11 @@
#ifdef LIBA52_DJBFFT
if (mm_accel & MM_ACCEL_DJBFFT) {
- fprintf (stderr, "Using djbfft for IMDCT transform\n");
ifft128 = (void (*) (complex_t *)) fftc4_un128;
ifft64 = (void (*) (complex_t *)) fftc4_un64;
} else
#endif
{
- fprintf (stderr, "No accelerated IMDCT transform found\n");
ifft128 = ifft128_c;
ifft64 = ifft64_c;
}

View File

@@ -1,36 +0,0 @@
{
"name": "liba52",
"config-opts": [ "--enable-shared", "--disable-static" ],
"rm-configure": true,
"cleanup": [ "/bin/*a52*" ],
"sources": [
{
"type": "archive",
"url": "http://liba52.sourceforge.net/files/a52dec-0.7.4.tar.gz",
"sha256": "a21d724ab3b3933330194353687df82c475b5dfb997513eef4c25de6c865ec33"
},
{
"type": "patch",
"path": "a52dec-0.7.4-rpath64.patch"
},
{
"type": "patch",
"path": "a52dec-configure-optflags.patch"
},
{
"type": "patch",
"path": "liba52-silence.patch"
},
{
"type": "patch",
"path": "liba52-prefer-pic.patch"
},
{
"type":"script",
"commands":[
"autoreconf -fiv"
],
"dest-filename":"autogen.sh"
}
]
}

View File

@@ -1,19 +0,0 @@
{
"name": "libass",
"config-opts": [ "--enable-shared", "--disable-static" ],
"sources": [
{
"type": "git",
"url": "https://github.com/libass/libass.git",
"tag": "0.14.0",
"commit": "73284b676b12b47e17af2ef1b430527299e10c17"
},
{
"type":"script",
"commands":[
"autoreconf -fiv"
],
"dest-filename":"autogen.sh"
}
]
}

View File

@@ -1,19 +0,0 @@
{
"name": "libdvdcss",
"config-opts": [ "--enable-shared", "--disable-static" ],
"sources": [
{
"type": "git",
"url": "https://code.videolan.org/videolan/libdvdcss.git",
"tag": "1.4.2",
"commit": "7b7c185704567398627ad0f9a0d948a63514394b"
},
{
"type":"script",
"commands":[
"autoreconf -fiv"
],
"dest-filename":"autogen.sh"
}
]
}

View File

@@ -1,19 +0,0 @@
{
"name": "libdvdnav",
"config-opts": [ "--enable-shared", "--disable-static" ],
"sources": [
{
"type": "git",
"url": "https://code.videolan.org/videolan/libdvdnav.git",
"tag": "6.1.0",
"commit": "4f48efd43efb2e3372cb494a8893342e1fb507ae"
},
{
"type":"script",
"commands":[
"autoreconf -fiv"
],
"dest-filename":"autogen.sh"
}
]
}

View File

@@ -1,19 +0,0 @@
{
"name": "libdvdread",
"config-opts": [ "--enable-shared", "--disable-static" ],
"sources": [
{
"type": "git",
"url": "https://code.videolan.org/videolan/libdvdread.git",
"tag": "6.1.0",
"commit": "d413571ce39acd404523b6742ba361215f6ada68"
},
{
"type":"script",
"commands":[
"autoreconf -fiv"
],
"dest-filename":"autogen.sh"
}
]
}

View File

@@ -1,12 +0,0 @@
{
"name": "libgudev",
"config-opts": [ "--enable-shared", "--disable-static", "--disable-umockdev" ],
"sources": [
{
"type": "git",
"url": "https://gitlab.gnome.org/GNOME/libgudev.git",
"tag": "234",
"commit": "e9342ee019482a08fe435d6b656f8a6bdd196bce"
}
]
}

View File

@@ -1,10 +0,0 @@
--- libmpeg2/configure.ac 2016-01-20 15:31:37.933547037 +0100
+++ libmpeg2.new/configure.ac 2016-01-20 15:05:40.931231465 +0100
@@ -149,7 +149,6 @@
dnl Checks for typedefs, structures, and compiler characteristics.
AC_C_CONST
-AC_C_ALWAYS_INLINE
AC_C_RESTRICT
AC_C_BUILTIN_EXPECT
AC_C_BIGENDIAN

View File

@@ -1,24 +0,0 @@
{
"name": "libmpeg2",
"config-opts": [ "--enable-shared", "--disable-static" ],
"rm-configure": true,
"cleanup": [ "/bin/*mpeg2*" ],
"sources": [
{
"type": "archive",
"url": "http://libmpeg2.sourceforge.net/files/libmpeg2-0.5.1.tar.gz",
"sha256": "dee22e893cb5fc2b2b6ebd60b88478ab8556cb3b93f9a0d7ce8f3b61851871d4"
},
{
"type": "patch",
"path": "libmpeg2-inline.patch"
},
{
"type":"script",
"commands":[
"autoreconf -fiv"
],
"dest-filename":"autogen.sh"
}
]
}

View File

@@ -1,12 +0,0 @@
{
"name": "libsass",
"buildsystem": "meson",
"sources": [
{
"type": "git",
"url": "https://github.com/lazka/libsass.git",
"branch": "meson",
"commit": "302397c0c8ae2d7ab02f45ea461c2c3d768f248e"
}
]
}

View File

@@ -1,15 +0,0 @@
{
"name": "pango",
"buildsystem": "meson",
"config-opts": [
"-Dgtk_doc=false"
],
"sources": [
{
"type": "git",
"url": "https://gitlab.gnome.org/GNOME/pango.git",
"tag": "1.48.0",
"commit": "a39fea44c7c9f982fcca6d639929545dd3e09eb7"
}
]
}

View File

@@ -1,15 +0,0 @@
{
"name": "sassc",
"buildsystem": "meson",
"config-opts": [
"--wrap-mode=nofallback"
],
"sources": [
{
"type": "git",
"url": "https://github.com/lazka/sassc.git",
"branch": "meson",
"commit": "82803377c33247265d779af034eceb5949e78354"
}
]
}

View File

@@ -1,19 +0,0 @@
{
"name": "uchardet",
"buildsystem": "cmake",
"builddir": true,
"config-opts": [
"-DCMAKE_BUILD_TYPE=Release",
"-DCMAKE_INSTALL_LIBDIR=lib",
"-DBUILD_STATIC=OFF",
"-DBUILD_BINARY=OFF"
],
"sources": [
{
"type": "git",
"url": "https://gitlab.freedesktop.org/uchardet/uchardet.git",
"tag": "v0.0.7",
"commit": "59f68dbe5709d708b53ad5ea95c7349d7ee6ebe4"
}
]
}

View File

@@ -26,7 +26,7 @@
%global glib2_version 2.56.0
Name: clapper
Version: 0.1.0
Version: 0.3.0
Release: 1%{?dist}
Summary: Simple and modern GNOME media player
@@ -126,6 +126,15 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/*.desktop
%{_libdir}/%{appname}/
%changelog
* Fri Jun 18 2021 Rafostar <rafostar.github@gmail.com> - 0.3.0-1
- New version
* Mon Apr 19 2021 Rafostar <rafostar.github@gmail.com> - 0.2.1-1
- New version
* 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

105
src/actions.js Normal file
View File

@@ -0,0 +1,105 @@
const { Gtk } = imports.gi;
const Dialogs = imports.src.dialogs;
const Misc = imports.src.misc;
var actions = {
open_local: ['<Ctrl>O'],
export_playlist: ['<Ctrl>E'],
open_uri: ['<Ctrl>U'],
prefs: null,
shortcuts: ['F1', '<Ctrl>question'],
about: null,
progress_forward: ['Right'],
progress_backward: ['Left'],
next_chapter: ['<Shift>Right'],
prev_chapter: ['<Shift>Left'],
next_track: ['<Ctrl>Right'],
prev_track: ['<Ctrl>Left'],
volume_up: ['Up'],
volume_down: ['Down'],
toggle_play: ['space'],
change_repeat: ['<Ctrl>r'],
reveal_controls: ['Return'],
toggle_fullscreen: ['F11', 'f'],
quit: ['<Ctrl>q', 'q'],
};
function handleAction(action, window)
{
const clapperWidget = window.child;
if(!clapperWidget) return;
const { player } = clapperWidget;
let bool = false;
switch(action.name) {
case 'open_local':
case 'export_playlist':
new Dialogs.FileChooser(window, action.name);
break;
case 'open_uri':
new Dialogs.UriDialog(window);
break;
case 'prefs':
new Dialogs.PrefsDialog(window);
break;
case 'shortcuts':
if(!window.get_help_overlay()) {
const clapperPath = Misc.getClapperPath();
const helpBuilder = Gtk.Builder.new_from_file(
`${clapperPath}/ui/help-overlay.ui`
);
window.set_help_overlay(helpBuilder.get_object('help_overlay'));
}
clapperWidget.activate_action('win.show-help-overlay', null);
break;
case 'about':
new Dialogs.AboutDialog(window);
break;
case 'progress_forward':
bool = true;
case 'progress_backward':
player.adjust_position(bool);
if(
clapperWidget.isReleaseKeyEnabled
&& clapperWidget.isFullscreenMode
)
clapperWidget.revealControls();
/* Actual seek is handled on release */
clapperWidget.isReleaseKeyEnabled = true;
if(!clapperWidget.has_focus)
clapperWidget.grab_focus();
break;
case 'volume_up':
bool = true;
case 'volume_down':
player.adjust_volume(bool);
break;
case 'next_track':
player.playlistWidget.nextTrack();
break;
case 'prev_track':
player.playlistWidget.prevTrack();
break;
case 'reveal_controls':
if(clapperWidget.isFullscreenMode)
clapperWidget.revealControls();
break;
case 'toggle_fullscreen':
clapperWidget.toggleFullscreen();
break;
case 'change_repeat':
player.playlistWidget.changeRepeatMode();
break;
case 'quit':
clapperWidget.activate_action('window.close', null);
break;
case 'toggle_play':
case 'next_chapter':
case 'prev_chapter':
player[action.name]();
break;
default:
break;
}
}

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,32 @@ 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.activate();
this._openFilesAsync(files).then(() => this.activate()).catch(debug);
}
_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

@@ -1,7 +1,8 @@
const { Gio, GLib, GObject, Gtk } = imports.gi;
const Debug = imports.src.debug;
const Menu = imports.src.menu;
const FileOps = imports.src.fileOps;
const Misc = imports.src.misc;
const Actions = imports.src.actions;
const { debug } = Debug;
const { settings } = Misc;
@@ -16,6 +17,7 @@ class ClapperAppBase extends Gtk.Application
});
this.doneFirstActivate = false;
this.isFileAppend = false;
}
vfunc_startup()
@@ -33,21 +35,7 @@ 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)
);
this.add_action(simpleAction);
}
window.add_css_class('gpufriendlyfs');
}
vfunc_activate()
@@ -62,8 +50,50 @@ class ClapperAppBase extends Gtk.Application
);
}
async _openFilesAsync(files)
{
const urisArr = [];
for(let file of files) {
const uri = file.get_uri();
if(!uri.startsWith('file:')) {
urisArr.push(uri);
continue;
}
/* If file is not a dir its URI will be returned in an array */
const uris = await FileOps.getDirFilesUrisPromise(file).catch(debug);
if(uris && uris.length)
urisArr.push(...uris);
}
const [playlist, subs] = Misc.parsePlaylistFiles(urisArr);
const { player } = this.active_window.get_child();
const action = (this.isFileAppend) ? 'append' : 'set';
if(playlist && playlist.length)
player[`${action}_playlist`](playlist);
if(subs)
player.set_subtitles(subs);
/* Restore default behavior */
this.isFileAppend = false;
}
_onFirstActivate()
{
for(let name in Actions.actions) {
const simpleAction = new Gio.SimpleAction({ name });
simpleAction.connect('activate', (action) =>
Actions.handleAction(action, this.active_window)
);
this.add_action(simpleAction);
const accels = Actions.actions[name];
if(accels)
this.set_accels_for_action(`app.${name}`, accels);
}
const gtkSettings = Gtk.Settings.get_default();
settings.bind(
'dark-theme', gtkSettings,
@@ -71,24 +101,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 +120,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 +141,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,11 +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 */
if(this.icon_name)
this.set_icon_name(this.icon_name);
@@ -79,10 +78,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 +94,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,18 +108,18 @@ 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 */
if(this.icon_name)
this.set_icon_name(this.icon_name);
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))
@@ -151,8 +148,6 @@ class ClapperPopoverButtonBase extends Gtk.ToggleButton
{
const clapperWidget = this.get_ancestor(Gtk.Grid);
clapperWidget.player.widget.grab_focus();
/* Set again timeout as popover is now closed */
if(clapperWidget.isFullscreenMode)
clapperWidget.revealControls();
@@ -189,7 +184,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);
}

68
src/controls.js vendored
View File

@@ -4,12 +4,11 @@ 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;
const INITIAL_ELAPSED = '00:00/00:00';
var Controls = GObject.registerClass(
class ClapperControls extends Gtk.Box
{
@@ -17,15 +16,13 @@ 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,
});
this.minFullViewWidth = 560;
this.currentPosition = 0;
this.currentDuration = 0;
this.isPositionDragging = false;
this.isMobile = false;
this.isFullscreen = false;
@@ -89,11 +86,6 @@ class ClapperControls extends Gtk.Box
this.unfullscreenButton.connect('clicked', this._onUnfullscreenClicked.bind(this));
this.unfullscreenButton.set_visible(false);
const keyController = new Gtk.EventControllerKey();
keyController.connect('key-pressed', this._onControlsKeyPressed.bind(this));
keyController.connect('key-released', this._onControlsKeyReleased.bind(this));
this.add_controller(keyController);
this.add_css_class('playercontrols');
this.realizeSignal = this.connect('realize', this._onRealize.bind(this));
}
@@ -107,8 +99,6 @@ class ClapperControls extends Gtk.Box
button.setFullscreenMode(isFullscreen);
this.unfullscreenButton.visible = isFullscreen;
this.can_focus = isFullscreen;
this.isFullscreen = isFullscreen;
}
@@ -120,6 +110,21 @@ class ClapperControls extends Gtk.Box
this.positionScale.visible = isSeekable;
}
setInitialState()
{
this.currentPosition = 0;
this.positionScale.set_value(0);
this.positionScale.visible = false;
this.elapsedButton.set_label(INITIAL_ELAPSED);
this.togglePlayButton.setPrimaryIcon();
for(let type of ['video', 'audio', 'subtitle'])
this[`${type}TracksButton`].visible = false;
this.visualizationsButton.visible = false;
}
updateElapsedLabel(value)
{
value = value || 0;
@@ -242,9 +247,6 @@ class ClapperControls extends Gtk.Box
}
if(checkButton.activeId < 0) {
if(checkButton.type === 'video')
clapperWidget.player.draw_black(true);
return clapperWidget.player[
`set_${checkButton.type}_track_enabled`
](false);
@@ -254,9 +256,6 @@ class ClapperControls extends Gtk.Box
clapperWidget.player[setTrack](checkButton.activeId);
clapperWidget.player[`${setTrack}_enabled`](true);
if(checkButton.type === 'video')
clapperWidget.player.draw_black(false);
}
_handleVisualizationChange(checkButton)
@@ -302,7 +301,7 @@ class ClapperControls extends Gtk.Box
_addElapsedButton()
{
const elapsedRevealer = new Revealers.ButtonsRevealer('SLIDE_RIGHT');
this.elapsedButton = this.addElapsedPopoverButton('00:00/00:00', elapsedRevealer);
this.elapsedButton = this.addElapsedPopoverButton(INITIAL_ELAPSED, elapsedRevealer);
elapsedRevealer.set_reveal_child(true);
this.revealersArr.push(elapsedRevealer);
@@ -439,7 +438,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,
@@ -479,7 +478,7 @@ class ClapperControls extends Gtk.Box
_onPlayerResize(width, height)
{
const isMobile = (width < 560);
const isMobile = (width < this.minFullViewWidth);
if(this.isMobile === isMobile)
return;
@@ -625,29 +624,6 @@ class ClapperControls extends Gtk.Box
}
}
/* Only happens when navigating through controls panel */
_onControlsKeyPressed(controller, keyval, keycode, state)
{
const clapperWidget = this.get_ancestor(Gtk.Grid);
clapperWidget._setHideControlsTimeout();
}
_onControlsKeyReleased(controller, keyval, keycode, state)
{
switch(keyval) {
case Gdk.KEY_space:
case Gdk.KEY_Return:
case Gdk.KEY_Escape:
case Gdk.KEY_Right:
case Gdk.KEY_Left:
break;
default:
const { player } = this.get_ancestor(Gtk.Grid);
player._onWidgetKeyReleased(controller, keyval, keycode, state);
break;
}
}
_onCloseRequest()
{
debug('controls close request');

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,60 @@ 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 _logStructured(debuggerName, msg, level)
{
levelName = levelName || 'LEVEL_DEBUG';
if(msg.message) {
levelName = 'LEVEL_CRITICAL';
msg = msg.message;
}
if(levelName !== 'LEVEL_CRITICAL')
return clapperDebug(msg);
GLib.log_structured(
'Clapper', GLib.LogLevelFlags[levelName], {
debuggerName, level, {
MESSAGE: msg,
SYSLOG_IDENTIFIER: 'clapper'
SYSLOG_IDENTIFIER: debuggerName.toLowerCase()
});
}
function _debug(debuggerName, msg)
{
if(msg.message) {
_logStructured(
debuggerName,
msg.message,
GLib.LogLevelFlags.LEVEL_CRITICAL
);
return;
}
switch(debuggerName) {
case 'Clapper':
clapperDebugger.debug(msg);
break;
case 'YouTube':
ytDebugger.debug(msg);
break;
}
}
function debug(msg)
{
_debug('Clapper', msg);
}
function ytDebug(msg)
{
_debug('YouTube', msg);
}
function warn(msg)
{
_logStructured('Clapper', msg, GLib.LogLevelFlags.LEVEL_WARNING);
}

View File

@@ -1,6 +1,7 @@
const { Gio, GObject, Gtk, Gst } = imports.gi;
const System = imports.system;
const Debug = imports.src.debug;
const FileOps = imports.src.fileOps;
const Misc = imports.src.misc;
const Prefs = imports.src.prefs;
const PrefsBase = imports.src.prefsBase;
@@ -10,31 +11,57 @@ const { debug } = Debug;
var FileChooser = GObject.registerClass(
class ClapperFileChooser extends Gtk.FileChooserNative
{
_init(window)
_init(window, purpose)
{
super._init({
transient_for: window,
modal: true,
select_multiple: true,
});
switch(purpose) {
case 'open_local':
this._prepareOpenLocal();
break;
case 'export_playlist':
this._prepareExportPlaylist();
break;
default:
debug(new Error(`unknown file chooser purpose: ${purpose}`));
break;
}
this.chooserPurpose = purpose;
this.responseSignal = this.connect('response', this._onResponse.bind(this));
/* File chooser closes itself when nobody is holding its ref */
this.ref();
this.show();
}
_prepareOpenLocal()
{
this.select_multiple = true;
const filter = new Gtk.FileFilter({
name: 'Media Files',
});
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));
_prepareExportPlaylist()
{
this.action = Gtk.FileChooserAction.SAVE;
this.set_current_name('playlist.claps');
/* File chooser closes itself when nobody is holding its ref */
this.ref();
this.show();
const filter = new Gtk.FileFilter({
name: 'Playlist Files',
});
filter.add_mime_type('application/claps');
this.add_filter(filter);
}
_onResponse(filechooser, response)
@@ -45,40 +72,58 @@ class ClapperFileChooser extends Gtk.FileChooserNative
this.responseSignal = null;
if(response === Gtk.ResponseType.ACCEPT) {
const files = this.get_files();
const playlist = [];
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);
index++;
switch(this.chooserPurpose) {
case 'open_local':
this._handleOpenLocal();
break;
case 'export_playlist':
this._handleExportPlaylist();
break;
}
const { player } = this.get_transient_for().get_child();
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);
}
this.unref();
this.destroy();
}
_handleOpenLocal()
{
const files = this.get_files();
const filesArray = [];
let index = 0;
let file;
while((file = files.get_item(index))) {
filesArray.push(file);
index++;
}
const { application } = this.transient_for;
const isHandlesOpen = Boolean(
application.flags & Gio.ApplicationFlags.HANDLES_OPEN
);
/* Remote app does not handle open */
if(isHandlesOpen)
application.open(filesArray, "");
else
application._openFilesAsync(filesArray);
}
_handleExportPlaylist()
{
const file = this.get_file();
const { playlistWidget } = this.transient_for.child.player;
const playlist = playlistWidget.getPlaylist(true);
FileOps.saveFileSimplePromise(file, playlist.join('\n'))
.then(() => {
debug(`exported playlist to file: ${file.get_path()}`);
})
.catch(err => {
debug(err);
});
}
});
@@ -125,8 +170,6 @@ class ClapperUriDialog extends Gtk.Dialog
area.append(box);
this.closeSignal = this.connect('close-request', this._onCloseRequest.bind(this));
this.ref();
this.show();
}
@@ -241,6 +284,10 @@ class ClapperPrefsDialog extends Gtk.Dialog
{
title: 'Network',
widget: Prefs.NetworkPage,
},
{
title: 'YouTube',
widget: Prefs.YouTubePage,
}
]
},
@@ -266,8 +313,6 @@ class ClapperPrefsDialog extends Gtk.Dialog
area.append(prefsNotebook);
this.closeSignal = this.connect('close-request', this._onCloseRequest.bind(this));
this.ref();
this.show();
}
@@ -328,8 +373,6 @@ class ClapperAboutDialog extends Gtk.AboutDialog
});
this.closeSignal = this.connect('close-request', this._onCloseRequest.bind(this));
this.ref();
this.show();
}

227
src/fileOps.js Normal file
View File

@@ -0,0 +1,227 @@
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');
Gio._promisify(LocalFilePrototype, 'query_info_async', 'query_info_finish');
Gio._promisify(LocalFilePrototype, 'enumerate_children_async', 'enumerate_children_finish');
Gio._promisify(Gio.FileEnumerator.prototype, 'close_async', 'close_finish');
Gio._promisify(Gio.FileEnumerator.prototype, 'next_files_async', 'next_files_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));
});
}
/* Simple save data to GioFile */
function saveFileSimplePromise(file, data)
{
return file.replace_contents_bytes_async(
GLib.Bytes.new_take(data),
null,
false,
Gio.FileCreateFlags.NONE,
null
);
}
/* 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);
saveFileSimplePromise(destFile, data)
.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));
});
}
function _getDirUrisPromise(dir, isDeep)
{
return new Promise(async (resolve, reject) => {
const enumerator = await dir.enumerate_children_async(
'standard::name,standard::type',
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_DEFAULT,
null
).catch(debug);
if(!enumerator)
return reject(new Error('could not create file enumerator'));
const dirPath = dir.get_path();
const arr = [];
debug(`enumerating files in dir: ${dirPath}`);
while(true) {
const infos = await enumerator.next_files_async(
1,
GLib.PRIORITY_DEFAULT,
null
).catch(debug);
if(!infos || !infos.length)
break;
const fileUri = dir.get_uri() + '/' + infos[0].get_name();
if(infos[0].get_file_type() !== Gio.FileType.DIRECTORY) {
arr.push(fileUri);
continue;
}
if(!isDeep)
continue;
const subDir = Misc.getFileFromLocalUri(fileUri);
const subDirUris = await _getDirUrisPromise(subDir, isDeep).catch(debug);
if(subDirUris && subDirUris.length)
arr.push(...subDirUris);
}
const isClosed = await enumerator.close_async(
GLib.PRIORITY_DEFAULT,
null
).catch(debug);
if(isClosed)
debug(`closed enumerator for dir: ${dirPath}`);
else
debug(new Error(`could not close file enumerator for dir: ${dirPath}`));
resolve(arr);
});
}
/* Either GioFile or URI for dir arg */
function getDirFilesUrisPromise(dir, isDeep)
{
return new Promise(async (resolve, reject) => {
if(!dir.get_path)
dir = Misc.getFileFromLocalUri(dir);
if(!dir)
return reject(new Error('invalid directory'));
const fileInfo = await dir.query_info_async(
'standard::type',
Gio.FileQueryInfoFlags.NONE,
GLib.PRIORITY_DEFAULT,
null
).catch(debug);
if(!fileInfo)
return reject(new Error('no file type info'));
if(fileInfo.get_file_type() !== Gio.FileType.DIRECTORY)
return resolve([dir.get_uri()]);
const arr = await _getDirUrisPromise(dir, isDeep).catch(debug);
if(!arr || !arr.length)
return reject(new Error('enumerated files list is empty'));
resolve(arr.sort());
});
}

View File

@@ -37,6 +37,7 @@ class ClapperHeaderBarBase extends Gtk.Box
this.menuButton = new Gtk.MenuButton({
icon_name: 'open-menu-symbolic',
valign: Gtk.Align.CENTER,
can_focus: false,
});
const mainMenuModel = uiBuilder.get_object('mainMenu');
const mainMenuPopover = new HeaderBarPopover(mainMenuModel);
@@ -53,6 +54,7 @@ class ClapperHeaderBarBase extends Gtk.Box
const floatButton = new Gtk.Button({
icon_name: 'go-bottom-symbolic',
can_focus: false,
});
floatButton.add_css_class('circular');
floatButton.add_css_class('linkedleft');
@@ -69,6 +71,7 @@ class ClapperHeaderBarBase extends Gtk.Box
const fullscreenButton = new Gtk.Button({
icon_name: 'view-fullscreen-symbolic',
can_focus: false,
});
fullscreenButton.add_css_class('circular');
fullscreenButton.add_css_class('linkedright');
@@ -151,7 +154,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 === 'icon'))
name = 'menu';
const widget = this[`${name}Widget`];
@@ -196,6 +199,7 @@ class ClapperHeaderBarBase extends Gtk.Box
const button = new Gtk.Button({
icon_name: `window-${name}-symbolic`,
valign: Gtk.Align.CENTER,
can_focus: false,
});
button.add_css_class('circular');
@@ -263,7 +267,5 @@ class ClapperHeaderBarPopover extends Gtk.PopoverMenu
child.revealControls();
child.isPopoverOpen = false;
child.player.widget.grab_focus();
}
});

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

@@ -1,13 +0,0 @@
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),
};
var accels = [
['app.quit', ['q']],
];

View File

@@ -1,10 +1,14 @@
const { Gio, GstAudio, Gdk, Gtk } = imports.gi;
const { Gio, GLib, 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',
'text/x-ssa',
];
var clapperPath = null;
var clapperVersion = null;
@@ -15,6 +19,16 @@ var settings = new Gio.Settings({
var maxVolume = 1.5;
/* Keys must be lowercase */
const subsTitles = {
sdh: 'SDH',
cc: 'CC',
traditional: 'Traditional',
simplified: 'Simplified',
honorifics: 'Honorifics',
};
const subsKeys = Object.keys(subsTitles);
let inhibitCookie;
function getClapperPath()
@@ -35,6 +49,68 @@ function getClapperVersion()
: '';
}
function getClapperThemeIconUri()
{
const display = Gdk.Display.get_default();
if(!display) return null;
const iconTheme = Gtk.IconTheme.get_for_display(display);
if(!iconTheme || !iconTheme.has_icon(appId))
return null;
const iconPaintable = iconTheme.lookup_icon(appId, null, 256, 1,
Gtk.TextDirection.NONE, Gtk.IconLookupFlags.FORCE_REGULAR
);
const iconFile = iconPaintable.get_file();
if(!iconFile) return null;
const iconPath = iconFile.get_path();
if(!iconPath) return null;
let substractName = iconPath.substring(
iconPath.indexOf('/icons/') + 7, iconPath.indexOf('/scalable/')
);
if(!substractName || substractName.includes('/'))
return null;
substractName = substractName.toLowerCase();
const postFix = (substractName === iconTheme.theme_name.toLowerCase())
? substractName
: 'hicolor';
const cacheIconName = `clapper-${postFix}.svg`;
/* We need to have this icon placed in a folder
* accessible from both app runtime and gnome-shell */
const expectedFile = Gio.File.new_for_path(
GLib.get_user_cache_dir() + `/${appId}/icons/${cacheIconName}`
);
if(!expectedFile.query_exists(null)) {
debug('no cached icon file');
const dirPath = expectedFile.get_parent().get_path();
GLib.mkdir_with_parents(dirPath, 493); // octal 755
iconFile.copy(expectedFile,
Gio.FileCopyFlags.TARGET_DEFAULT_PERMS, null, null
);
debug(`icon copied to cache dir: ${cacheIconName}`);
}
const iconUri = expectedFile.get_uri();
debug(`using cached clapper icon uri: ${iconUri}`);
return iconUri;
}
function getSubsTitle(infoTitle)
{
if(!infoTitle)
return null;
const searchName = infoTitle.toLowerCase();
const found = subsKeys.find(key => key === searchName);
return (found) ? subsTitles[found] : null;
}
function loadCustomCss()
{
const clapperPath = getClapperPath();
@@ -95,3 +171,69 @@ 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;
}
/* JS replacement of "Gst.Uri.get_protocol" */
function getUriProtocol(uri)
{
const arr = uri.split(':');
return (arr.length > 1) ? arr[0] : null;
}
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,32 +2,59 @@ const { Gdk, Gio, GObject, Gst, GstClapper, Gtk } = imports.gi;
const ByteArray = imports.byteArray;
const Debug = imports.src.debug;
const Misc = imports.src.misc;
const { PlayerBase } = imports.src.playerBase;
const YouTube = imports.src.youtube;
const { PlaylistWidget } = imports.src.playlist;
const { WebApp } = imports.src.webApp;
const { debug } = Debug;
const { debug, warn } = Debug;
const { settings } = Misc;
let WebServer;
var Player = GObject.registerClass(
class ClapperPlayer extends PlayerBase
class ClapperPlayer extends GstClapper.Clapper
{
_init()
{
super._init();
const gtk4plugin = new GstClapper.ClapperGtk4Plugin();
const glsinkbin = Gst.ElementFactory.make('glsinkbin', null);
glsinkbin.sink = gtk4plugin.video_sink;
super._init({
signal_dispatcher: new GstClapper.ClapperGMainContextSignalDispatcher(),
video_renderer: new GstClapper.ClapperVideoOverlayVideoRenderer({
video_sink: glsinkbin,
}),
mpris: new GstClapper.ClapperMpris({
own_name: `org.mpris.MediaPlayer2.${Misc.appName}`,
id_path: '/' + Misc.appId.replace(/\./g, '/'),
identity: Misc.appName,
desktop_entry: Misc.appId,
default_art_url: Misc.getClapperThemeIconUri(),
}),
});
this.widget = gtk4plugin.video_sink.widget;
this.widget.add_css_class('videowidget');
this.visualization_enabled = false;
this.webserver = null;
this.webapp = null;
this.ytClient = null;
this.playlistWidget = new PlaylistWidget();
this.seek_done = true;
this.doneStartup = false;
this.needsFastSeekRestore = false;
this.customVideoTitle = null;
this.playOnFullscreen = false;
this.windowMapped = false;
this.quitOnStop = false;
this.needsTocUpdate = true;
this.keyPressCount = 0;
const keyController = new Gtk.EventControllerKey();
keyController.connect('key-pressed', this._onWidgetKeyPressed.bind(this));
keyController.connect('key-released', this._onWidgetKeyReleased.bind(this));
this.widget.add_controller(keyController);
this.set_all_plugins_ranks();
this.set_initial_config();
this.set_and_bind_settings();
this.connect('state-changed', this._onStateChanged.bind(this));
this.connect('uri-loaded', this._onUriLoaded.bind(this));
@@ -35,25 +62,117 @@ class ClapperPlayer extends PlayerBase
this.connect('warning', this._onPlayerWarning.bind(this));
this.connect('error', this._onPlayerError.bind(this));
settings.connect('changed', this._onSettingsKeyChanged.bind(this));
this._realizeSignal = this.widget.connect('realize', this._onWidgetRealize.bind(this));
}
set_and_bind_settings()
{
const settingsToSet = [
'seeking-mode',
'audio-offset',
'subtitle-offset',
'play-flags',
'webserver-enabled'
];
for(let key of settingsToSet)
this._onSettingsKeyChanged(settings, key);
const flag = Gio.SettingsBindFlags.GET;
settings.bind('keep-last-frame', this.widget, 'keep-last-frame', flag);
settings.bind('subtitle-font', this.pipeline, 'subtitle-font-desc', flag);
}
set_initial_config()
{
this.set_mute(false);
/* FIXME: change into option in preferences */
const pipeline = this.get_pipeline();
pipeline.ring_buffer_max_size = 8 * 1024 * 1024;
}
set_all_plugins_ranks()
{
let data = [];
/* Set empty plugin list if someone messed it externally */
try {
data = JSON.parse(settings.get_string('plugin-ranking'));
if(!Array.isArray(data))
throw new Error('plugin ranking data is not an array!');
}
catch(err) {
debug(err);
settings.set_string('plugin-ranking', "[]");
}
for(let plugin of data) {
if(!plugin.apply || !plugin.name)
continue;
this.set_plugin_rank(plugin.name, plugin.rank);
}
}
set_plugin_rank(name, rank)
{
const gstRegistry = Gst.Registry.get();
const feature = gstRegistry.lookup_feature(name);
if(!feature) {
warn(`cannot change rank of unavailable plugin: ${name}`);
return;
}
const oldRank = feature.get_rank();
if(rank === oldRank)
return;
feature.set_rank(rank);
debug(`changed rank: ${oldRank} -> ${rank} for ${name}`);
}
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(Misc.getUriProtocol(uri) !== 'file') {
const [isYouTubeUri, videoId] = YouTube.checkYouTubeUri(uri);
if(!isYouTubeUri)
return super.set_uri(uri);
if(!this.ytClient)
this.ytClient = new YouTube.YouTubeClient();
const { root } = this.widget;
const surface = root.get_surface();
const monitor = root.display.get_monitor_at_surface(surface);
this.ytClient.getPlaybackDataAsync(videoId, monitor)
.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,36 +205,58 @@ 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();
const firstTrack = this.playlistWidget.get_row_at_index(0);
if(!firstTrack) return;
debug('new playlist');
this.playlistWidget.removeAll();
this._addPlaylistItems(playlist);
firstTrack.activate();
if(settings.get_boolean('fullscreen-auto')) {
const { root } = this.playlistWidget;
/* Do not enter fullscreen when already in it
* or when in floating mode */
if(
root
&& root.child
&& !root.child.isFullscreenMode
&& root.child.controlsRevealer.reveal_child
)
root.fullscreen();
}
/* If not mapped yet, first track will play after map */
if(this.windowMapped)
this._playFirstTrack();
}
append_playlist(playlist)
{
debug('appending playlist');
this._addPlaylistItems(playlist);
if(
!this.windowMapped
|| this.state !== GstClapper.ClapperState.STOPPED
)
return;
if(!this.playlistWidget.nextTrack())
debug('playlist append failed');
}
set_subtitles(source)
{
const uri = (source.get_uri)
? source.get_uri()
: source;
const uri = this._getSourceUri(source);
/* Check local file existence */
if(
Misc.getUriProtocol(uri) === 'file'
&& !Misc.getFileFromLocalUri(uri)
)
return;
this.set_subtitle_uri(uri);
this.set_subtitle_track_enabled(true);
@@ -158,7 +299,7 @@ class ClapperPlayer extends PlayerBase
seek_seconds(seconds)
{
this.seek(seconds * 1000000000);
this.seek(seconds * Gst.SECOND);
}
seek_chapter(seconds)
@@ -168,12 +309,9 @@ class ClapperPlayer extends PlayerBase
return;
}
/* FIXME: Remove this check when GstPlay(er) have set_seek_mode function */
if(this.set_seek_mode) {
this.set_seek_mode(GstClapper.ClapperSeekMode.DEFAULT);
this.seekingMode = 'normal';
this.needsFastSeekRestore = true;
}
this.set_seek_mode(GstClapper.ClapperSeekMode.DEFAULT);
this.seekingMode = 'normal';
this.needsFastSeekRestore = true;
this.seek_seconds(seconds);
}
@@ -221,13 +359,22 @@ class ClapperPlayer extends PlayerBase
controls.volumeScale.set_value(volume);
}
toggle_play()
next_chapter()
{
const action = (this.state === GstClapper.ClapperState.PLAYING)
? 'pause'
: 'play';
return this._switchChapter(false);
}
this[action]();
prev_chapter()
{
return this._switchChapter(true);
}
emitWs(action, value)
{
if(!this.webserver)
return;
this.webserver.sendMessage({ action, value });
}
receiveWs(action, value)
@@ -236,9 +383,26 @@ class ClapperPlayer extends PlayerBase
case 'toggle_play':
case 'play':
case 'pause':
this[action]();
break;
case 'seek':
case 'set_playlist':
case 'append_playlist':
case 'set_subtitles':
this[action](value);
break;
case 'change_playlist_item':
this.playlistWidget.changeActiveRow(value);
break;
case 'toggle_fullscreen':
case 'volume_up':
case 'volume_down':
case 'next_track':
case 'prev_track':
case 'next_chapter':
case 'prev_chapter':
this.widget.activate_action(`app.${action}`, null);
break;
case 'toggle_maximized':
action = 'toggle-maximized';
case 'minimize':
@@ -246,20 +410,64 @@ class ClapperPlayer extends PlayerBase
this.widget.activate_action(`window.${action}`, null);
break;
default:
const clapperWidget = this.widget.get_ancestor(Gtk.Grid);
switch(action) {
case 'toggle_fullscreen':
clapperWidget.toggleFullscreen();
break;
default:
super.receiveWs(action, value);
break;
}
warn(`unhandled WebSocket action: ${action}`);
break;
}
}
_switchChapter(isPrevious)
{
if(this.state === GstClapper.ClapperState.STOPPED)
return false;
const { chapters } = this.widget.root.child.controls;
if(!chapters)
return false;
const now = this.position / Gst.SECOND;
const chapterTimes = Object.keys(chapters).sort((a, b) => a - b);
if(isPrevious)
chapterTimes.reverse();
const chapter = chapterTimes.find(time => (isPrevious)
? now - 2.5 > time
: now < time
);
if(!chapter)
return false;
this.seek_chapter(chapter);
return true;
}
_addPlaylistItems(playlist)
{
for(let source of playlist) {
const uri = this._getSourceUri(source);
debug(`added uri: ${uri}`);
this.playlistWidget.addItem(uri);
}
}
_getSourceUri(source)
{
return (source.get_uri != null)
? source.get_uri()
: Gst.uri_is_valid(source)
? source
: Gst.filename_to_uri(source);
}
_playFirstTrack()
{
const firstTrack = this.playlistWidget.get_row_at_index(0);
if(!firstTrack) return;
firstTrack.activate();
}
_performCloseCleanup(window)
{
window.disconnect(this.closeRequestSignal);
@@ -277,21 +485,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) {
let resumeInfo = {};
if(settings.get_boolean('resume-enabled')) {
const resumeTitle = this.playlistWidget.getActiveFilename();
const resumeTime = Math.floor(this.position / 1000000000);
const resumeDuration = this.duration / 1000000000;
const playlistItem = this.playlistWidget.getActiveRow();
/* Do not save resume info when title is too long (random URI),
* video is very short, just started or almost finished */
let resumeInfo = {};
if(playlistItem.isLocalFile && settings.get_boolean('resume-enabled')) {
const resumeTime = Math.floor(this.position / Gst.SECOND);
const resumeDuration = this.duration / Gst.SECOND;
/* 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;
@@ -312,7 +520,6 @@ class ClapperPlayer extends PlayerBase
_onStateChanged(player, state)
{
this.state = state;
this.emitWs('state_changed', state);
if(state !== GstClapper.ClapperState.BUFFERING) {
@@ -356,7 +563,7 @@ class ClapperPlayer extends PlayerBase
debug(`end of stream: ${lastTrackId}`);
this.emitWs('end_of_stream', lastTrackId);
if(this.playlistWidget.nextTrack())
if(this.playlistWidget._handleStreamEnded(player))
return;
if(settings.get_boolean('close-auto')) {
@@ -364,6 +571,10 @@ class ClapperPlayer extends PlayerBase
this.quitOnStop = true;
this._performCloseCleanup(this.widget.get_root());
}
/* When this signal is connected player
* wants us to decide if it should stop */
this.stop();
}
_onUriLoaded(player, uri)
@@ -371,26 +582,12 @@ class ClapperPlayer extends PlayerBase
debug(`URI loaded: ${uri}`);
this.needsTocUpdate = true;
if(!this.doneStartup) {
this.doneStartup = true;
if(settings.get_boolean('fullscreen-auto')) {
const root = player.widget.get_root();
const clapperWidget = root.get_child();
if(!clapperWidget.isFullscreenMode) {
this.playOnFullscreen = true;
root.fullscreen();
return;
}
}
}
this.play();
player.play();
}
_onPlayerWarning(player, error)
{
debug(error.message, 'LEVEL_WARNING');
debug(error.message);
}
_onPlayerError(player, error)
@@ -421,75 +618,10 @@ class ClapperPlayer extends PlayerBase
);
}
/* Widget only - does not happen when using controls navigation */
_onWidgetKeyPressed(controller, keyval, keycode, state)
_onWindowMap(window)
{
const clapperWidget = this.widget.get_ancestor(Gtk.Grid);
let bool = false;
this.keyPressCount++;
switch(keyval) {
case Gdk.KEY_Up:
bool = true;
case Gdk.KEY_Down:
this.adjust_volume(bool);
break;
case Gdk.KEY_Right:
bool = true;
case Gdk.KEY_Left:
this.adjust_position(bool);
if(this.keyPressCount > 1)
clapperWidget.revealControls();
break;
default:
break;
}
}
/* Also happens after using controls navigation for selected keys */
_onWidgetKeyReleased(controller, keyval, keycode, state)
{
const clapperWidget = this.widget.get_ancestor(Gtk.Grid);
let value, root;
this.keyPressCount = 0;
switch(keyval) {
case Gdk.KEY_space:
this.toggle_play();
break;
case Gdk.KEY_Return:
if(clapperWidget.isFullscreenMode)
clapperWidget.revealControls(true);
break;
case Gdk.KEY_Right:
case Gdk.KEY_Left:
value = Math.round(
clapperWidget.controls.positionScale.get_value()
);
this.seek_seconds(value);
clapperWidget._setHideControlsTimeout();
break;
case Gdk.KEY_F11:
case Gdk.KEY_f:
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();
root.emit('close-request');
break;
default:
break;
}
this.windowMapped = true;
this._playFirstTrack();
}
_onCloseRequest(window)
@@ -502,4 +634,109 @@ class ClapperPlayer extends PlayerBase
this.quitOnStop = true;
this.stop();
}
_onSettingsKeyChanged(settings, key)
{
let root, value, action;
switch(key) {
case 'seeking-mode':
this.seekingMode = settings.get_string('seeking-mode');
switch(this.seekingMode) {
case 'fast':
this.set_seek_mode(GstClapper.ClapperSeekMode.FAST);
break;
case 'accurate':
this.set_seek_mode(GstClapper.ClapperSeekMode.ACCURATE);
break;
default:
this.set_seek_mode(GstClapper.ClapperSeekMode.DEFAULT);
break;
}
break;
case 'render-shadows':
root = this.widget.get_root();
if(!root) break;
const gpuClass = 'gpufriendly';
const renderShadows = settings.get_boolean(key);
const hasShadows = !root.has_css_class(gpuClass);
if(renderShadows === hasShadows)
break;
action = (renderShadows) ? 'remove' : 'add';
root[action + '_css_class'](gpuClass);
break;
case 'audio-offset':
value = Math.round(settings.get_double(key) * -Gst.MSECOND);
this.set_audio_video_offset(value);
debug(`set audio-video offset: ${value}`);
break;
case 'subtitle-offset':
value = Math.round(settings.get_double(key) * -Gst.MSECOND);
this.set_subtitle_video_offset(value);
debug(`set subtitle-video offset: ${value}`);
break;
case 'dark-theme':
root = this.widget.get_root();
if(!root) break;
root.application._onThemeChanged(Gtk.Settings.get_default());
break;
case 'play-flags':
const initialFlags = this.pipeline.flags;
const settingsFlags = settings.get_int(key);
if(initialFlags === settingsFlags)
break;
this.pipeline.flags = settingsFlags;
debug(`changed play flags: ${initialFlags} -> ${settingsFlags}`);
break;
case 'webserver-enabled':
case 'webapp-enabled':
const webserverEnabled = settings.get_boolean('webserver-enabled');
if(webserverEnabled) {
if(!WebServer) {
/* Probably most users will not use this,
* so conditional import for faster startup */
WebServer = imports.src.webServer.WebServer;
}
if(!this.webserver) {
this.webserver = new WebServer(settings.get_int('webserver-port'));
this.webserver.passMsgData = this.receiveWs.bind(this);
}
this.webserver.startListening();
const webappEnabled = settings.get_boolean('webapp-enabled');
if(!this.webapp && !webappEnabled)
break;
if(webappEnabled) {
if(!this.webapp)
this.webapp = new WebApp();
this.webapp.startDaemonApp(settings.get_int('webapp-port'));
}
}
else if(this.webserver) {
/* remote app will close when connection is lost
* which will cause the daemon to close too */
this.webserver.stopListening();
}
break;
case 'webserver-port':
if(!this.webserver)
break;
this.webserver.setListeningPort(settings.get_int(key));
break;
default:
break;
}
}
});

View File

@@ -1,256 +0,0 @@
const { Gio, GLib, GObject, Gst, GstClapper, Gtk } = imports.gi;
const Debug = imports.src.debug;
const Misc = imports.src.misc;
const { PlaylistWidget } = imports.src.playlist;
const { WebApp } = imports.src.webApp;
const { debug } = Debug;
const { settings } = Misc;
let WebServer;
var PlayerBase = GObject.registerClass(
class ClapperPlayerBase extends GstClapper.Clapper
{
_init()
{
const gtk4plugin = new GstClapper.ClapperGtk4Plugin();
const glsinkbin = Gst.ElementFactory.make('glsinkbin', null);
glsinkbin.sink = gtk4plugin.video_sink;
const dispatcher = new GstClapper.ClapperGMainContextSignalDispatcher();
const renderer = new GstClapper.ClapperVideoOverlayVideoRenderer({
video_sink: glsinkbin
});
super._init({
signal_dispatcher: dispatcher,
video_renderer: renderer
});
this.widget = gtk4plugin.video_sink.widget;
this.widget.add_css_class('videowidget');
this.state = GstClapper.ClapperState.STOPPED;
this.visualization_enabled = false;
this.webserver = null;
this.webapp = null;
this.playlistWidget = new PlaylistWidget();
this.set_all_plugins_ranks();
this.set_initial_config();
this.set_and_bind_settings();
settings.connect('changed', this._onSettingsKeyChanged.bind(this));
}
set_and_bind_settings()
{
const settingsToSet = [
'seeking-mode',
'audio-offset',
'subtitle-offset',
'play-flags',
'webserver-enabled'
];
for(let key of settingsToSet)
this._onSettingsKeyChanged(settings, key);
const flag = Gio.SettingsBindFlags.GET;
settings.bind('subtitle-font', this.pipeline, 'subtitle_font_desc', flag);
}
set_initial_config()
{
this.set_mute(false);
/* FIXME: change into option in preferences */
const pipeline = this.get_pipeline();
pipeline.ring_buffer_max_size = 8 * 1024 * 1024;
}
set_all_plugins_ranks()
{
let data = [];
/* Set empty plugin list if someone messed it externally */
try {
data = JSON.parse(settings.get_string('plugin-ranking'));
if(!Array.isArray(data))
throw new Error('plugin ranking data is not an array!');
}
catch(err) {
debug(err);
settings.set_string('plugin-ranking', "[]");
}
for(let plugin of data) {
if(!plugin.apply || !plugin.name)
continue;
this.set_plugin_rank(plugin.name, plugin.rank);
}
}
set_plugin_rank(name, rank)
{
const gstRegistry = Gst.Registry.get();
const feature = gstRegistry.lookup_feature(name);
if(!feature)
return debug(`plugin unavailable: ${name}`);
const oldRank = feature.get_rank();
if(rank === oldRank)
return;
feature.set_rank(rank);
debug(`changed rank: ${oldRank} -> ${rank} for ${name}`);
}
draw_black(isEnabled)
{
this.widget.ignore_textures = isEnabled;
if(this.state !== GstClapper.ClapperState.PLAYING)
this.widget.queue_render();
}
emitWs(action, value)
{
if(!this.webserver)
return;
this.webserver.sendMessage({ action, value });
}
receiveWs(action, value)
{
debug(`unhandled WebSocket action: ${action}`);
}
_onSettingsKeyChanged(settings, key)
{
let root, value, action;
switch(key) {
case 'seeking-mode':
this.seekingMode = settings.get_string('seeking-mode');
switch(this.seekingMode) {
case 'fast':
this.set_seek_mode(GstClapper.ClapperSeekMode.FAST);
break;
case 'accurate':
this.set_seek_mode(GstClapper.ClapperSeekMode.ACCURATE);
break;
default:
this.set_seek_mode(GstClapper.ClapperSeekMode.DEFAULT);
break;
}
break;
case 'render-shadows':
root = this.widget.get_root();
/* Editing theme of someone else app is taboo */
if(!root || !root.isClapperApp)
break;
const gpuClass = 'gpufriendly';
const renderShadows = settings.get_boolean(key);
const hasShadows = !root.has_css_class(gpuClass);
if(renderShadows === hasShadows)
break;
action = (renderShadows) ? 'remove' : 'add';
root[action + '_css_class'](gpuClass);
break;
case 'audio-offset':
value = Math.round(settings.get_double(key) * -1000000);
this.set_audio_video_offset(value);
debug(`set audio-video offset: ${value}`);
break;
case 'subtitle-offset':
value = Math.round(settings.get_double(key) * -1000000);
this.set_subtitle_video_offset(value);
debug(`set subtitle-video offset: ${value}`);
break;
case 'dark-theme':
case 'brighter-sliders':
root = this.widget.get_root();
if(!root || !root.isClapperApp)
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`);
break;
case 'play-flags':
const initialFlags = this.pipeline.flags;
const settingsFlags = settings.get_int(key);
if(initialFlags === settingsFlags)
break;
this.pipeline.flags = settingsFlags;
debug(`changed play flags: ${initialFlags} -> ${settingsFlags}`);
break;
case 'webserver-enabled':
case 'webapp-enabled':
const webserverEnabled = settings.get_boolean('webserver-enabled');
if(webserverEnabled) {
if(!WebServer) {
/* Probably most users will not use this,
* so conditional import for faster startup */
WebServer = imports.src.webServer.WebServer;
}
if(!this.webserver) {
this.webserver = new WebServer(settings.get_int('webserver-port'));
this.webserver.passMsgData = this.receiveWs.bind(this);
}
this.webserver.startListening();
const webappEnabled = settings.get_boolean('webapp-enabled');
if(!this.webapp && !webappEnabled)
break;
if(webappEnabled) {
if(!this.webapp)
this.webapp = new WebApp();
this.webapp.startDaemonApp(settings.get_int('webapp-port'));
}
}
else if(this.webserver) {
/* remote app will close when connection is lost
* which will cause the daemon to close too */
this.webserver.stopListening();
}
break;
case 'webserver-port':
if(!this.webserver)
break;
this.webserver.setListeningPort(settings.get_int(key));
break;
default:
break;
}
}
});

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

Some files were not shown because too many files have changed in this diff Show More