88 Commits
0.2.1 ... 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
89 changed files with 2694 additions and 1945 deletions

2
.gitattributes vendored
View File

@@ -1,3 +1,5 @@
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

@@ -36,7 +36,8 @@ Additionally it also has a few patches, thus some functionalities work better (o
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:

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

@@ -70,7 +70,7 @@ radio {
min-height: 180px;
}
.tvmode popover box {
.fullscreen.tvmode popover box {
text-shadow: none;
font-size: 21px;
font-weight: 500;
@@ -87,17 +87,17 @@ radio {
margin-left: 1px;
margin-right: 1px;
}
.tvmode .playercontrols button {
.fullscreen.tvmode .playercontrols button {
min-width: 32px;
min-height: 32px;
margin: 5px;
margin-left: 3px;
margin-right: 3px;
}
.tvmode button image {
.fullscreen.tvmode button image {
-gtk-icon-shadow: none;
}
.tvmode radio {
.fullscreen.tvmode radio {
margin-left: 0px;
margin-right: 4px;
border: 2px solid;
@@ -105,13 +105,13 @@ radio {
min-height: 17px;
}
.tvmode .playercontrols button image {
.fullscreen.tvmode .playercontrols button image {
-gtk-icon-size: 24px;
}
.adwicons .playbackicon {
-gtk-icon-size: 20px;
}
.adwicons.tvmode .playbackicon {
.adwicons.fullscreen.tvmode .playbackicon {
-gtk-icon-size: 28px;
}
.labelbuttonlabel {
@@ -122,21 +122,21 @@ radio {
font-variant-numeric: tabular-nums;
font-weight: 600;
}
.tvmode .labelbuttonlabel {
.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;
@@ -144,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;
@@ -171,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;
@@ -183,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;
}
@@ -199,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;
}
@@ -221,7 +221,7 @@ radio {
margin-right: -6px;
min-height: 180px;
}
.tvmode .volumescale {
.fullscreen.tvmode .volumescale {
margin: 2px;
margin-left: -6px;
margin-right: -4px;
@@ -232,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 {
@@ -246,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;
}
@@ -272,7 +272,7 @@ radio {
.chapterlabel {
min-width: 32px;
}
.tvmode .chapterlabel {
.fullscreen.tvmode .chapterlabel {
min-width: 40px;
text-shadow: none;
font-size: 22px;
@@ -314,7 +314,7 @@ radio {
.gpufriendly {
box-shadow: -8px -8px transparent, 8px 8px transparent;
}
.gpufriendlyfs {
.fullscreen.gpufriendlyfs {
box-shadow: none;
}

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>

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,26 @@
</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>

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

@@ -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,
@@ -125,6 +129,7 @@ struct _GstClapper
GstClapperVideoRenderer *video_renderer;
GstClapperSignalDispatcher *signal_dispatcher;
GstClapperMpris *mpris;
gchar *uri;
gchar *redirect_uri;
@@ -139,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;
@@ -169,6 +174,10 @@ 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;
@@ -268,6 +277,8 @@ gst_clapper_init (GstClapper * self)
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");
}
@@ -297,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);
@@ -478,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);
@@ -494,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)
@@ -550,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) {
@@ -566,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;
@@ -638,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);
@@ -730,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);
@@ -964,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
@@ -1015,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;
@@ -1129,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;
@@ -1311,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
@@ -1564,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,
@@ -1632,6 +1686,15 @@ state_changed_cb (G_GNUC_UNUSED GstBus * bus, GstMessage * msg,
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
@@ -1743,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);
@@ -1876,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)
@@ -1975,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)
{
@@ -2029,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
@@ -2075,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
@@ -2273,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)
@@ -2593,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)
{
@@ -2709,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);
@@ -2818,9 +2932,21 @@ mute_notify_cb (G_GNUC_UNUSED GObject * obj, G_GNUC_UNUSED GParamSpec * pspec,
static void
element_setup_cb (GstElement * playbin, GstElement * element, GstClapper * self)
{
GParamSpec *prop = g_object_class_find_property (G_OBJECT_GET_CLASS (element),
"user-agent");
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";
@@ -2871,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);
}
}
@@ -2892,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),
@@ -2914,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);
@@ -2946,7 +3089,6 @@ gst_clapper_main (gpointer data)
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;
@@ -2990,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
@@ -3003,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;
}
@@ -3040,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);
@@ -3057,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;
}
@@ -3086,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,
@@ -3128,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;
}
@@ -3157,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
@@ -3186,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);
@@ -3297,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;
@@ -3437,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)
{
@@ -3448,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
@@ -3651,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
@@ -3695,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) {
@@ -3727,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) {
@@ -3759,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) {
@@ -3816,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,
@@ -3857,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);
@@ -3899,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,
@@ -4038,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

@@ -37,11 +37,6 @@
GST_DEBUG_CATEGORY (gst_debug_clapper_gl_sink);
#define GST_CAT_DEFAULT gst_debug_clapper_gl_sink
#define DEFAULT_FORCE_ASPECT_RATIO TRUE
#define DEFAULT_PAR_N 0
#define DEFAULT_PAR_D 1
#define DEFAULT_IGNORE_TEXTURES FALSE
static GstStaticPadTemplate gst_clapper_gl_sink_template =
GST_STATIC_PAD_TEMPLATE ("sink",
GST_PAD_SINK,
@@ -63,6 +58,7 @@ static gboolean gst_clapper_gl_sink_propose_allocation (GstBaseSink * bsink,
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,
@@ -80,15 +76,6 @@ static GstFlowReturn gst_clapper_gl_sink_show_frame (GstVideoSink * bsink,
static void
gst_clapper_gl_sink_navigation_interface_init (GstNavigationInterface * iface);
enum
{
PROP_0,
PROP_WIDGET,
PROP_FORCE_ASPECT_RATIO,
PROP_PIXEL_ASPECT_RATIO,
PROP_IGNORE_TEXTURES,
};
#define gst_clapper_gl_sink_parent_class parent_class
G_DEFINE_TYPE_WITH_CODE (GstClapperGLSink, gst_clapper_gl_sink,
GST_TYPE_VIDEO_SINK,
@@ -114,6 +101,7 @@ gst_clapper_gl_sink_class_init (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",
@@ -121,24 +109,7 @@ gst_clapper_gl_sink_class_init (GstClapperGLSinkClass * klass)
"(must only be get from the GTK main thread)",
GTK_TYPE_WIDGET, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (gobject_class, PROP_FORCE_ASPECT_RATIO,
g_param_spec_boolean ("force-aspect-ratio",
"Force aspect ratio",
"When enabled, scaling will respect original aspect ratio",
DEFAULT_FORCE_ASPECT_RATIO,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (gobject_class, PROP_PIXEL_ASPECT_RATIO,
gst_param_spec_fraction ("pixel-aspect-ratio", "Pixel Aspect Ratio",
"The pixel aspect ratio of the device", DEFAULT_PAR_N, DEFAULT_PAR_D,
G_MAXINT, 1, 1, 1, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (gobject_class, PROP_IGNORE_TEXTURES,
g_param_spec_boolean ("ignore-textures", "Ignore Textures",
"When enabled, textures will be ignored and not drawn",
DEFAULT_IGNORE_TEXTURES, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
gobject_class->finalize = gst_clapper_gl_sink_finalize;
gst_gtk_install_shared_properties (gobject_class);
gstelement_class->change_state = gst_clapper_gl_sink_change_state;
@@ -149,6 +120,7 @@ gst_clapper_gl_sink_class_init (GstClapperGLSinkClass * klass)
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;
@@ -173,7 +145,9 @@ gst_clapper_gl_sink_init (GstClapperGLSink * clapper_sink)
clapper_sink->force_aspect_ratio = DEFAULT_FORCE_ASPECT_RATIO;
clapper_sink->par_n = DEFAULT_PAR_N;
clapper_sink->par_d = DEFAULT_PAR_D;
clapper_sink->ignore_textures = DEFAULT_IGNORE_TEXTURES;
clapper_sink->keep_last_frame = DEFAULT_KEEP_LAST_FRAME;
clapper_sink->had_eos = FALSE;
}
static void
@@ -236,15 +210,12 @@ gst_clapper_gl_sink_get_widget (GstClapperGLSink * clapper_sink)
clapper_sink->widget = (GtkClapperGLWidget *)
GST_CLAPPER_GL_SINK_GET_CLASS (clapper_sink)->create_widget ();
clapper_sink->bind_aspect_ratio =
g_object_bind_property (clapper_sink, "force-aspect-ratio", clapper_sink->widget,
g_object_bind_property (clapper_sink, "force-aspect-ratio", clapper_sink->widget,
"force-aspect-ratio", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
clapper_sink->bind_pixel_aspect_ratio =
g_object_bind_property (clapper_sink, "pixel-aspect-ratio", clapper_sink->widget,
g_object_bind_property (clapper_sink, "pixel-aspect-ratio", clapper_sink->widget,
"pixel-aspect-ratio", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
clapper_sink->bind_ignore_textures =
g_object_bind_property (clapper_sink, "ignore-textures", clapper_sink->widget,
"ignore-textures", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
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. */
@@ -290,8 +261,8 @@ gst_clapper_gl_sink_get_property (GObject * object, guint prop_id,
case PROP_PIXEL_ASPECT_RATIO:
gst_value_set_fraction (value, clapper_sink->par_n, clapper_sink->par_d);
break;
case PROP_IGNORE_TEXTURES:
g_value_set_boolean (value, clapper_sink->ignore_textures);
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);
@@ -313,8 +284,8 @@ gst_clapper_gl_sink_set_property (GObject * object, guint prop_id,
clapper_sink->par_n = gst_value_get_fraction_numerator (value);
clapper_sink->par_d = gst_value_get_fraction_denominator (value);
break;
case PROP_IGNORE_TEXTURES:
clapper_sink->ignore_textures = g_value_get_boolean (value);
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);
@@ -604,8 +575,17 @@ gst_clapper_gl_sink_change_state (GstElement * element, GstStateChange transitio
return ret;
switch (transition) {
case GST_STATE_CHANGE_READY_TO_PAUSED:
{
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);
@@ -619,6 +599,16 @@ gst_clapper_gl_sink_change_state (GstElement * element, GstStateChange transitio
}
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)
@@ -706,6 +696,29 @@ gst_clapper_gl_sink_set_caps (GstBaseSink * bsink, GstCaps * caps)
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)
{

View File

@@ -58,15 +58,14 @@ struct _GstClapperGLSink
GtkClapperGLWidget *widget;
gboolean had_eos;
/* properties */
gboolean force_aspect_ratio;
GBinding *bind_aspect_ratio;
gint par_n, par_d;
GBinding *bind_pixel_aspect_ratio;
gboolean keep_last_frame;
gboolean ignore_textures;
GBinding *bind_ignore_textures;
GtkWidget *window;
gulong widget_destroy_id;

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

View File

@@ -32,8 +32,13 @@
#if GST_GL_HAVE_WINDOW_X11 && defined (GDK_WINDOWING_X11)
#include <gdk/x11/gdkx.h>
#if GST_GL_HAVE_PLATFORM_EGL
#include <gst/gl/egl/gstgldisplay_egl.h>
#endif
#if GST_GL_HAVE_PLATFORM_GLX
#include <gst/gl/x11/gstgldisplay_x11.h>
#endif
#endif
#if GST_GL_HAVE_WINDOW_WAYLAND && defined (GDK_WINDOWING_WAYLAND)
#include <gdk/wayland/gdkwayland.h>
@@ -52,11 +57,6 @@
GST_DEBUG_CATEGORY (gst_debug_clapper_gl_widget);
#define GST_CAT_DEFAULT gst_debug_clapper_gl_widget
#define DEFAULT_FORCE_ASPECT_RATIO TRUE
#define DEFAULT_PAR_N 0
#define DEFAULT_PAR_D 1
#define DEFAULT_IGNORE_TEXTURES FALSE
struct _GtkClapperGLWidgetPrivate
{
gboolean initiated;
@@ -90,14 +90,6 @@ G_DEFINE_TYPE_WITH_CODE (GtkClapperGLWidget, gtk_clapper_gl_widget, GTK_TYPE_GL_
GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "gtkclapperglwidget", 0,
"GTK Clapper GL Widget"));
enum
{
PROP_0,
PROP_FORCE_ASPECT_RATIO,
PROP_PIXEL_ASPECT_RATIO,
PROP_IGNORE_TEXTURES,
};
static void
gtk_clapper_gl_widget_get_preferred_width (GtkWidget * widget, gint * min,
gint * natural)
@@ -171,8 +163,8 @@ gtk_clapper_gl_widget_set_property (GObject * object, guint prop_id,
clapper_widget->par_n = gst_value_get_fraction_numerator (value);
clapper_widget->par_d = gst_value_get_fraction_denominator (value);
break;
case PROP_IGNORE_TEXTURES:
clapper_widget->ignore_textures = g_value_get_boolean (value);
case PROP_KEEP_LAST_FRAME:
clapper_widget->keep_last_frame = g_value_get_boolean (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
@@ -193,8 +185,8 @@ gtk_clapper_gl_widget_get_property (GObject * object, guint prop_id,
case PROP_PIXEL_ASPECT_RATIO:
gst_value_set_fraction (value, clapper_widget->par_n, clapper_widget->par_d);
break;
case PROP_IGNORE_TEXTURES:
g_value_set_boolean (value, clapper_widget->ignore_textures);
case PROP_KEEP_LAST_FRAME:
g_value_set_boolean (value, clapper_widget->keep_last_frame);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
@@ -448,10 +440,13 @@ gtk_clapper_gl_widget_motion_event (GtkEventControllerMotion * motion_controller
GtkClapperGLWidget *clapper_widget = GTK_CLAPPER_GL_WIDGET (widget);
GstElement *element;
if ((element = g_weak_ref_get (&clapper_widget->element))) {
if (x != clapper_widget->last_pos_x && y != clapper_widget->last_pos_y &&
(element = g_weak_ref_get (&clapper_widget->element))) {
if (GST_IS_NAVIGATION (element)) {
gdouble stream_x, stream_y;
clapper_widget->last_pos_x = x;
clapper_widget->last_pos_y = y;
_display_size_to_stream_size (clapper_widget, x, y, &stream_x, &stream_y);
gst_navigation_send_mouse_event (GST_NAVIGATION (element), "mouse-move",
@@ -463,6 +458,13 @@ gtk_clapper_gl_widget_motion_event (GtkEventControllerMotion * motion_controller
return FALSE;
}
static void
gtk_clapper_gl_widget_settings_changed (GtkGLArea * glarea)
{
GST_DEBUG ("GTK settings changed, queued render");
gtk_gl_area_queue_render (glarea);
}
static void
gtk_clapper_gl_widget_bind_buffer (GtkClapperGLWidget * clapper_widget)
{
@@ -566,7 +568,7 @@ gtk_clapper_gl_widget_render (GtkGLArea * widget, GdkGLContext * context)
/* 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 || clapper_widget->ignore_textures) {
if (!priv->context || !priv->other_context || clapper_widget->ignore_buffers) {
_draw_black_with_gdk (context);
goto done;
}
@@ -845,27 +847,23 @@ _get_gl_context (GtkClapperGLWidget * clapper_widget)
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);
}
if (GST_IS_GL_DISPLAY_EGL (priv->display)) {
platform = GST_GL_PLATFORM_EGL;
gl_handle = gst_gl_context_get_current_gl_context (platform);
}
#endif
if (gl_handle) {
gl_api = _get_current_gl_api (platform);
priv->other_context =
gst_gl_context_new_wrapped (priv->display, gl_handle,
platform, gl_api);
}
#if GST_GL_HAVE_PLATFORM_GLX
if (!gl_handle && GST_IS_GL_DISPLAY_X11 (priv->display)) {
platform = GST_GL_PLATFORM_GLX;
gl_handle = gst_gl_context_get_current_gl_context (platform);
}
#endif
if (gl_handle) {
gl_api = _get_current_gl_api (platform);
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)
@@ -906,35 +904,20 @@ _get_gl_context (GtkClapperGLWidget * clapper_widget)
static void
gtk_clapper_gl_widget_class_init (GtkClapperGLWidgetClass * klass)
{
GObjectClass *gobject_klass = (GObjectClass *) klass;
GtkWidgetClass *widget_klass = (GtkWidgetClass *) klass;
GtkGLAreaClass *gl_area_klass = (GtkGLAreaClass *) klass;
GObjectClass *gobject_class = (GObjectClass *) klass;
GtkWidgetClass *widget_class = (GtkWidgetClass *) klass;
GtkGLAreaClass *gl_area_class = (GtkGLAreaClass *) klass;
gobject_klass->set_property = gtk_clapper_gl_widget_set_property;
gobject_klass->get_property = gtk_clapper_gl_widget_get_property;
gobject_klass->finalize = gtk_clapper_gl_widget_finalize;
gobject_class->set_property = gtk_clapper_gl_widget_set_property;
gobject_class->get_property = gtk_clapper_gl_widget_get_property;
gobject_class->finalize = gtk_clapper_gl_widget_finalize;
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));
gst_gtk_install_shared_properties (gobject_class);
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));
widget_class->measure = gtk_clapper_gl_widget_measure;
widget_class->size_allocate = gtk_clapper_gl_widget_size_allocate;
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));
widget_klass->measure = gtk_clapper_gl_widget_measure;
widget_klass->size_allocate = gtk_clapper_gl_widget_size_allocate;
gl_area_klass->render = gtk_clapper_gl_widget_render;
gl_area_class->render = gtk_clapper_gl_widget_render;
}
static void
@@ -947,7 +930,10 @@ gtk_clapper_gl_widget_init (GtkClapperGLWidget * clapper_widget)
clapper_widget->force_aspect_ratio = DEFAULT_FORCE_ASPECT_RATIO;
clapper_widget->par_n = DEFAULT_PAR_N;
clapper_widget->par_d = DEFAULT_PAR_D;
clapper_widget->ignore_textures = DEFAULT_IGNORE_TEXTURES;
clapper_widget->keep_last_frame = DEFAULT_KEEP_LAST_FRAME;
clapper_widget->ignore_buffers = FALSE;
clapper_widget->last_pos_x = 0;
clapper_widget->last_pos_y = 0;
gst_video_info_init (&clapper_widget->v_info);
gst_video_info_init (&clapper_widget->pending_v_info);
@@ -992,9 +978,20 @@ gtk_clapper_gl_widget_init (GtkClapperGLWidget * clapper_widget)
#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));
gpointer display_ptr;
#if GST_GL_HAVE_PLATFORM_EGL && GTK_CHECK_VERSION(4,3,1)
display_ptr = gdk_x11_display_get_egl_display (display);
if (display_ptr)
priv->display = (GstGLDisplay *)
gst_gl_display_egl_new_with_egl_display (display_ptr);
#endif
#if GST_GL_HAVE_PLATFORM_GLX
if (!priv->display) {
display_ptr = gdk_x11_display_get_xdisplay (display);
priv->display = (GstGLDisplay *)
gst_gl_display_x11_new_with_display (display_ptr);
}
#endif
}
#endif
#if GST_GL_HAVE_WINDOW_WAYLAND && defined (GDK_WINDOWING_WAYLAND)
@@ -1014,6 +1011,9 @@ gtk_clapper_gl_widget_init (GtkClapperGLWidget * clapper_widget)
GST_INFO ("Created %" GST_PTR_FORMAT, priv->display);
gtk_gl_area_set_auto_render (GTK_GL_AREA (widget), FALSE);
g_signal_connect_swapped (gtk_widget_get_settings (widget), "notify",
G_CALLBACK (gtk_clapper_gl_widget_settings_changed), GTK_GL_AREA (widget));
}
GtkWidget *

View File

@@ -52,7 +52,7 @@ struct _GtkClapperGLWidget
/* properties */
gboolean force_aspect_ratio;
gint par_n, par_d;
gboolean ignore_textures;
gboolean keep_last_frame;
gint display_width;
gint display_height;
@@ -61,7 +61,12 @@ struct _GtkClapperGLWidget
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;

View File

@@ -1,3 +1,5 @@
gnome = import('gnome')
gstclapper_sources = [
'gstclapper.c',
'gstclapper-signal-dispatcher.c',
@@ -6,6 +8,7 @@ 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/gstclapperglsink.c',
@@ -23,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 = [
@@ -40,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
@@ -51,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.2.1',
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.2.1
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.2.1) unstable; urgency=low
clapper (0.3.0) unstable; urgency=low
* New version
-- Rafostar <rafostar.github@gmail.com> Mon, 19 Apr 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

@@ -13,38 +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": [
"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,86 +0,0 @@
From be0f4bc94fad9fe182c97eef389954b5f63f7092 Mon Sep 17 00:00:00 2001
From: Jun Xie <jun.xie@samsung.com>
Date: Sat, 4 Nov 2017 14:48:54 +0800
Subject: [PATCH] dashdemux: fix segmentBase type with 'sidx' not using range
download issue
1. for utilizing range download and enable bitrate switch
* update fragment info after 'sidx' is downloaded and parsed,
so that media segment's range is set by 'sidx' entry info.
* while updating fragment info, setting range_end by 'sidx' entry size.
2. for singleSegmentBase type WITHOUT @indexRange explicitly presented in MPD file
* set '*sidx_seek_needed' to true, early terminate currently no-range downloading whole file,
then jump to the requested SIDX entry by using sidx info.
3. for 'ref type 1' 'sidx'
* keep current behaviour for 'ref type 1', download as a whole file without range download
https://bugzilla.gnome.org/show_bug.cgi?id=788763
diff --git a/ext/dash/gstdashdemux.c b/ext/dash/gstdashdemux.c
index e38240800..7554a44b2 100644
--- a/ext/dash/gstdashdemux.c
+++ b/ext/dash/gstdashdemux.c
@@ -1356,7 +1356,7 @@ gst_dash_demux_stream_update_fragment_info (GstAdaptiveDemuxStream * stream)
stream->fragment.range_start + entry->size - 1;
dashstream->actual_position += entry->duration;
} else {
- stream->fragment.range_end = fragment.range_end;
+ stream->fragment.range_end = stream->fragment.range_start + entry->size - 1;
}
} else {
dashstream->actual_position = stream->fragment.timestamp =
@@ -1572,7 +1572,7 @@ gst_dash_demux_stream_has_next_subfragment (GstAdaptiveDemuxStream * stream)
if (dashstream->sidx_parser.status == GST_ISOFF_SIDX_PARSER_FINISHED) {
if (stream->demux->segment.rate > 0.0) {
- if (sidx->entry_index + 1 < sidx->entries_count)
+ if (sidx->entry_index < sidx->entries_count)
return TRUE;
} else {
if (sidx->entry_index >= 1)
@@ -2903,6 +2903,7 @@ gst_dash_demux_parse_isobmff (GstAdaptiveDemux * demux,
GstByteReader sub_reader;
GstIsoffParserResult res;
guint dummy;
+ gboolean ref_type1_found = FALSE;
dash_stream->sidx_base_offset =
dash_stream->isobmff_parser.current_start_offset + size;
@@ -2932,6 +2933,7 @@ gst_dash_demux_parse_isobmff (GstAdaptiveDemux * demux,
GST_FIXME_OBJECT (stream->pad, "SIDX ref_type 1 not supported yet");
dash_stream->sidx_position = GST_CLOCK_TIME_NONE;
gst_isoff_sidx_parser_clear (&dash_stream->sidx_parser);
+ ref_type1_found = TRUE;
break;
}
}
@@ -2968,8 +2970,9 @@ gst_dash_demux_parse_isobmff (GstAdaptiveDemux * demux,
}
}
- if (dash_stream->sidx_parser.status == GST_ISOFF_SIDX_PARSER_FINISHED &&
- SIDX (dash_stream)->entry_index != 0) {
+ if ((dash_stream->sidx_parser.status == GST_ISOFF_SIDX_PARSER_FINISHED &&
+ SIDX (dash_stream)->entry_index != 0) || (!stream->downloading_index &&
+ !ref_type1_found)) {
/* Need to jump to the requested SIDX entry. Push everything up to
* the SIDX box below and let the caller handle everything else */
*sidx_seek_needed = TRUE;
diff --git a/gst-libs/gst/adaptivedemux/gstadaptivedemux.c b/gst-libs/gst/adaptivedemux/gstadaptivedemux.c
index a495ec2e7..3a09a76b1 100644
--- a/gst-libs/gst/adaptivedemux/gstadaptivedemux.c
+++ b/gst-libs/gst/adaptivedemux/gstadaptivedemux.c
@@ -3378,6 +3378,9 @@ gst_adaptive_demux_stream_download_header_fragment (GstAdaptiveDemuxStream *
ret = gst_adaptive_demux_stream_download_uri (demux, stream,
stream->fragment.index_uri, stream->fragment.index_range_start,
stream->fragment.index_range_end, NULL);
+
+ gst_adaptive_demux_stream_update_fragment_info(stream->demux, stream);
+
stream->downloading_index = FALSE;
}
}
--
2.7.4

View File

@@ -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,45 +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"
},
{
"type": "patch",
"path": "gst-plugins-bad-dashdemux-sdix-range-download.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,34 +0,0 @@
From d42546dda8fdb3d044e715d0a6a1a74cd411acbe Mon Sep 17 00:00:00 2001
From: Rafostar <40623528+Rafostar@users.noreply.github.com>
Date: Mon, 5 Apr 2021 18:05:38 +0200
Subject: [PATCH] GL: Do not set backbuffer on Wayland memory copy
This aims to workaround a Mesa bug that causes crash on Intel GPUs
caused by calling "glDrawBuffer (GL_BACK)" on Wayland where
there is no actual backbuffer in GStreamer OpenGL context.
---
gst-libs/gst/gl/gstglmemory.c | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/gst-libs/gst/gl/gstglmemory.c b/gst-libs/gst/gl/gstglmemory.c
index 76c04eb1b..cd3481847 100644
--- a/gst-libs/gst/gl/gstglmemory.c
+++ b/gst-libs/gst/gl/gstglmemory.c
@@ -762,7 +762,13 @@ gst_gl_memory_copy_teximage (GstGLMemory * src, guint tex_id,
gl->DeleteFramebuffers (n_fbos, &fbo[0]);
if (gl->DrawBuffer)
- gl->DrawBuffer (GL_BACK);
+ gl->DrawBuffer (
+#if GST_GL_HAVE_WINDOW_WAYLAND
+ GL_NONE
+#else
+ GL_BACK
+#endif
+ );
}
gst_memory_unmap (GST_MEMORY_CAST (src), &sinfo);
--
2.28.0

View File

@@ -1,34 +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"
},
{
"type": "patch",
"path": "gst-plugins-base-do-not-set-backbuffer.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,30 +0,0 @@
{
"name": "ffmpeg",
"cleanup": [
"/lib/ffmpeg/examples"
],
"config-opts": [
"--disable-debug",
"--disable-doc",
"--disable-static",
"--disable-everything",
"--enable-gpl",
"--enable-version3",
"--enable-shared",
"--enable-optimizations",
"--enable-runtime-cpudetect",
"--enable-pthreads",
"--enable-protocol=file",
"--enable-decoder=flv,h263,h264,hevc,mjpeg,mpeg2video,mpeg4,mpegvideo,msmpeg4v1,msmpeg4v2,png,tiff,vc1,vp8,vp9,webp,wmv1,wmv2,wmv3,zerocodec",
"--enable-decoder=aac,aac_fixed,aac_latm,ac3,ac3_fixed,eac3,flac,mp3,opus,tak,truehd,tta,wmalossless",
"--enable-demuxer=gif,yuv4mpegpipe"
],
"sources": [
{
"type": "git",
"url": "https://git.ffmpeg.org/ffmpeg.git",
"tag": "n4.4",
"commit": "dc91b913b6260e85e1304c74ff7bb3c22a8c9fb1"
}
]
}

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,28 +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",
"commit": "5710df685b0af9b7dd306dfba6c7e174e428950e"
},
{
"type": "patch",
"path": "gtk4-popover-unrealize.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.2.1
Version: 0.3.0
Release: 1%{?dist}
Summary: Simple and modern GNOME media player
@@ -126,6 +126,9 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/*.desktop
%{_libdir}/%{appname}/
%changelog
* 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

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

@@ -38,8 +38,7 @@ class ClapperApp extends AppBase
{
super.vfunc_open(files, hint);
this._openFiles(files);
this.activate();
this._openFilesAsync(files).then(() => this.activate()).catch(debug);
}
_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,18 +35,7 @@ class ClapperAppBase extends Gtk.Application
if(!settings.get_boolean('render-shadows'))
window.add_css_class('gpufriendly');
for(let action in Menu.actions) {
const simpleAction = new Gio.SimpleAction({
name: action
});
simpleAction.connect(
'activate', () => Menu.actions[action].run(this.active_window)
);
this.add_action(simpleAction);
if(Menu.actions[action].accels)
this.set_accels_for_action(`app.${action}`, Menu.actions[action].accels);
}
window.add_css_class('gpufriendlyfs');
}
vfunc_activate()
@@ -59,19 +50,50 @@ class ClapperAppBase extends Gtk.Application
);
}
_openFiles(files)
async _openFilesAsync(files)
{
const [playlist, subs] = Misc.parsePlaylistFiles(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.set_playlist(playlist);
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,

View File

@@ -31,8 +31,6 @@ class ClapperCustomButton extends Gtk.Button
if(this.isFullscreen === isFullscreen)
return;
this.can_focus = isFullscreen;
/* Redraw icon after style class change */
if(this.icon_name)
this.set_icon_name(this.icon_name);
@@ -110,8 +108,6 @@ class ClapperPopoverButtonBase extends Gtk.ToggleButton
if(this.isFullscreen === isFullscreen)
return;
this.can_focus = isFullscreen;
/* Redraw icon after style class change */
if(this.icon_name)
this.set_icon_name(this.icon_name);
@@ -152,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();

56
src/controls.js vendored
View File

@@ -7,6 +7,8 @@ const Revealers = imports.src.revealers;
const { debug } = Debug;
const { settings } = Misc;
const INITIAL_ELAPSED = '00:00/00:00';
var Controls = GObject.registerClass(
class ClapperControls extends Gtk.Box
{
@@ -21,7 +23,6 @@ class ClapperControls extends Gtk.Box
this.minFullViewWidth = 560;
this.currentPosition = 0;
this.currentDuration = 0;
this.isPositionDragging = false;
this.isMobile = false;
this.isFullscreen = false;
@@ -85,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));
}
@@ -103,8 +99,6 @@ class ClapperControls extends Gtk.Box
button.setFullscreenMode(isFullscreen);
this.unfullscreenButton.visible = isFullscreen;
this.can_focus = isFullscreen;
this.isFullscreen = isFullscreen;
}
@@ -116,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;
@@ -238,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);
@@ -250,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)
@@ -298,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);
@@ -621,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');

View File

@@ -31,14 +31,23 @@ const ytDebugger = new Debug.Debugger('YouTube', {
high_precision: true,
});
function _debug(msg, debuggerName)
function _logStructured(debuggerName, msg, level)
{
GLib.log_structured(
debuggerName, level, {
MESSAGE: msg,
SYSLOG_IDENTIFIER: debuggerName.toLowerCase()
});
}
function _debug(debuggerName, msg)
{
if(msg.message) {
GLib.log_structured(
debuggerName, GLib.LogLevelFlags.LEVEL_CRITICAL, {
MESSAGE: msg.message,
SYSLOG_IDENTIFIER: debuggerName.toLowerCase()
});
_logStructured(
debuggerName,
msg.message,
GLib.LogLevelFlags.LEVEL_CRITICAL
);
return;
}
@@ -55,10 +64,15 @@ function _debug(msg, debuggerName)
function debug(msg)
{
_debug(msg, 'Clapper');
_debug('Clapper', msg);
}
function ytDebug(msg)
{
_debug(msg, 'YouTube');
_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,14 +11,37 @@ 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',
});
@@ -26,12 +50,18 @@ class ClapperFileChooser extends Gtk.FileChooserNative
filter.add_mime_type('application/claps');
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)
@@ -42,30 +72,58 @@ class ClapperFileChooser extends Gtk.FileChooserNative
this.responseSignal = null;
if(response === Gtk.ResponseType.ACCEPT) {
const files = this.get_files();
const filesArray = [];
let index = 0;
let file;
while((file = files.get_item(index))) {
filesArray.push(file);
index++;
switch(this.chooserPurpose) {
case 'open_local':
this._handleOpenLocal();
break;
case 'export_playlist':
this._handleExportPlaylist();
break;
}
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._openFiles(filesArray);
}
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);
});
}
});
@@ -112,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();
}
@@ -257,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();
}
@@ -319,8 +373,6 @@ class ClapperAboutDialog extends Gtk.AboutDialog
});
this.closeSignal = this.connect('close-request', this._onCloseRequest.bind(this));
this.ref();
this.show();
}

View File

@@ -12,6 +12,11 @@ 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()
{
@@ -52,6 +57,18 @@ function createDirPromise(dir)
});
}
/* 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)
{
@@ -77,18 +94,12 @@ function saveFilePromise(place, subdirName, fileName, data)
}
const destFile = destDir.get_child(fileName);
destFile.replace_contents_bytes_async(
GLib.Bytes.new_take(data),
null,
false,
Gio.FileCreateFlags.NONE,
null
)
.then(() => {
debug(`saved file: ${destPath}`);
resolve(destFile);
})
.catch(err => reject(err));
saveFileSimplePromise(destFile, data)
.then(() => {
debug(`saved file: ${destPath}`);
resolve(destFile);
})
.catch(err => reject(err));
});
}
@@ -126,3 +137,91 @@ function getFileContentsPromise(place, subdirName, fileName)
.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');
@@ -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,19 +0,0 @@
const { GObject, Gtk } = imports.gi;
const Dialogs = imports.src.dialogs;
var actions = {
openLocal: {
run: (window) => new Dialogs.FileChooser(window),
accels: ['<Ctrl>O'],
},
openUri: {
run: (window) => new Dialogs.UriDialog(window),
accels: ['<Ctrl>U'],
},
prefs: {
run: (window) => new Dialogs.PrefsDialog(window),
},
about: {
run: (window) => new Dialogs.AboutDialog(window),
},
};

View File

@@ -1,4 +1,4 @@
const { Gio, Gdk, Gtk } = imports.gi;
const { Gio, GLib, Gdk, Gtk } = imports.gi;
const Debug = imports.src.debug;
const { debug } = Debug;
@@ -7,6 +7,7 @@ var appName = 'Clapper';
var appId = 'com.github.rafostar.Clapper';
var subsMimes = [
'application/x-subrip',
'text/x-ssa',
];
var clapperPath = null;
@@ -18,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()
@@ -38,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();
@@ -139,6 +212,13 @@ function getFileFromLocalUri(uri)
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;')

View File

@@ -1,4 +1,4 @@
const { Gdk, Gio, GLib, GObject, Gst, GstClapper, Gtk } = imports.gi;
const { Gdk, Gio, GObject, Gst, GstClapper, Gtk } = imports.gi;
const ByteArray = imports.byteArray;
const Debug = imports.src.debug;
const Misc = imports.src.misc;
@@ -6,7 +6,7 @@ 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;
@@ -20,24 +20,28 @@ class ClapperPlayer extends GstClapper.Clapper
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
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.state = GstClapper.ClapperState.STOPPED;
this.visualization_enabled = false;
this.webserver = null;
this.webapp = null;
this.ytClient = null;
this.playlistWidget = new PlaylistWidget();
this.seek_done = true;
@@ -45,19 +49,9 @@ class ClapperPlayer extends GstClapper.Clapper
this.customVideoTitle = null;
this.windowMapped = false;
this.canAutoFullscreen = false;
this.playOnFullscreen = false;
this.quitOnStop = false;
this.needsTocUpdate = true;
this.keyPressCount = 0;
this.ytClient = null;
const keyController = new Gtk.EventControllerKey();
keyController.connect('key-pressed', this._onWidgetKeyPressed.bind(this));
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();
@@ -87,7 +81,8 @@ class ClapperPlayer extends GstClapper.Clapper
this._onSettingsKeyChanged(settings, key);
const flag = Gio.SettingsBindFlags.GET;
settings.bind('subtitle-font', this.pipeline, 'subtitle_font_desc', flag);
settings.bind('keep-last-frame', this.widget, 'keep-last-frame', flag);
settings.bind('subtitle-font', this.pipeline, 'subtitle-font-desc', flag);
}
set_initial_config()
@@ -126,8 +121,10 @@ class ClapperPlayer extends GstClapper.Clapper
{
const gstRegistry = Gst.Registry.get();
const feature = gstRegistry.lookup_feature(name);
if(!feature)
return debug(`plugin unavailable: ${name}`);
if(!feature) {
warn(`cannot change rank of unavailable plugin: ${name}`);
return;
}
const oldRank = feature.get_rank();
if(rank === oldRank)
@@ -137,19 +134,11 @@ class ClapperPlayer extends GstClapper.Clapper
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();
}
set_uri(uri)
{
this.customVideoTitle = null;
if(Gst.Uri.get_protocol(uri) !== 'file') {
if(Misc.getUriProtocol(uri) !== 'file') {
const [isYouTubeUri, videoId] = YouTube.checkYouTubeUri(uri);
if(!isYouTubeUri)
@@ -221,12 +210,21 @@ class ClapperPlayer extends GstClapper.Clapper
if(this.state !== GstClapper.ClapperState.STOPPED)
this.stop();
debug('new playlist');
this.playlistWidget.removeAll();
this.canAutoFullscreen = true;
this._addPlaylistItems(playlist);
for(let source of playlist) {
const uri = this._getSourceUri(source);
this.playlistWidget.addItem(uri);
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 */
@@ -234,13 +232,28 @@ class ClapperPlayer extends GstClapper.Clapper
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 = this._getSourceUri(source);
/* Check local file existence */
if(
Gst.Uri.get_protocol(uri) === 'file'
Misc.getUriProtocol(uri) === 'file'
&& !Misc.getFileFromLocalUri(uri)
)
return;
@@ -346,13 +359,14 @@ class ClapperPlayer extends GstClapper.Clapper
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)
@@ -369,10 +383,26 @@ class ClapperPlayer extends GstClapper.Clapper
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':
@@ -380,20 +410,47 @@ class ClapperPlayer extends GstClapper.Clapper
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:
debug(`unhandled WebSocket action: ${action}`);
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)
@@ -463,7 +520,6 @@ class ClapperPlayer extends GstClapper.Clapper
_onStateChanged(player, state)
{
this.state = state;
this.emitWs('state_changed', state);
if(state !== GstClapper.ClapperState.BUFFERING) {
@@ -507,7 +563,7 @@ class ClapperPlayer extends GstClapper.Clapper
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')) {
@@ -515,6 +571,10 @@ class ClapperPlayer extends GstClapper.Clapper
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)
@@ -522,26 +582,7 @@ class ClapperPlayer extends GstClapper.Clapper
debug(`URI loaded: ${uri}`);
this.needsTocUpdate = true;
if(this.canAutoFullscreen) {
this.canAutoFullscreen = false;
if(settings.get_boolean('fullscreen-auto')) {
const root = player.widget.get_root();
const clapperWidget = root.get_child();
/* Do not enter fullscreen when already in it
* or when in floating mode */
if(
!clapperWidget.isFullscreenMode
&& clapperWidget.controlsRevealer.reveal_child
) {
this.playOnFullscreen = true;
root.fullscreen();
return;
}
}
}
this.play();
player.play();
}
_onPlayerWarning(player, error)
@@ -577,71 +618,6 @@ class ClapperPlayer extends GstClapper.Clapper
);
}
/* Widget only - does not happen when using controls navigation */
_onWidgetKeyPressed(controller, keyval, keycode, state)
{
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_q:
case Gdk.KEY_Q:
root = this.widget.get_root();
root.emit('close-request');
break;
default:
break;
}
}
_onWindowMap(window)
{
this.windowMapped = true;

View File

@@ -1,4 +1,22 @@
const { Gdk, GLib, GObject, Gst, Gtk, Pango } = imports.gi;
const { Gdk, GLib, GObject, Gtk, Pango } = imports.gi;
const Debug = imports.src.debug;
const Misc = imports.src.misc;
const { debug, warn } = Debug;
var RepeatMode = {
NONE: 0,
TRACK: 1,
PLAYLIST: 2,
SHUFFLE: 3,
};
const repeatIcons = [
'media-playlist-consecutive-symbolic',
'media-playlist-repeat-song-symbolic',
'media-playlist-repeat-symbolic',
'media-playlist-shuffle-symbolic',
];
var PlaylistWidget = GObject.registerClass(
class ClapperPlaylistWidget extends Gtk.ListBox
@@ -9,6 +27,8 @@ class ClapperPlaylistWidget extends Gtk.ListBox
selection_mode: Gtk.SelectionMode.NONE,
});
this.activeRowId = -1;
this.repeatMode = RepeatMode.NONE;
this.connect('row-activated', this._onRowActivated.bind(this));
}
@@ -23,9 +43,7 @@ class ClapperPlaylistWidget extends Gtk.ListBox
const itemIndex = item.get_index();
if(itemIndex === this.activeRowId) {
const root = this.get_root();
root.emit('close-request');
this.activate_action('window.close', null);
return;
}
@@ -46,13 +64,12 @@ class ClapperPlaylistWidget extends Gtk.ListBox
nextTrack()
{
const nextRow = this.get_row_at_index(this.activeRowId + 1);
if(!nextRow)
return false;
return this._switchTrack(false);
}
nextRow.activate();
return true;
prevTrack()
{
return this._switchTrack(true);
}
getActiveRow()
@@ -60,6 +77,24 @@ class ClapperPlaylistWidget extends Gtk.ListBox
return this.get_row_at_index(this.activeRowId);
}
getPlaylist(useFilePaths)
{
const playlist = [];
let index = 0;
let item;
while((item = this.get_row_at_index(index))) {
const path = (useFilePaths && item.isLocalFile)
? GLib.filename_from_uri(item.uri)[0]
: item.uri;
playlist.push(path);
index++;
}
return playlist;
}
getActiveFilename()
{
const row = this.getActiveRow();
@@ -68,7 +103,43 @@ class ClapperPlaylistWidget extends Gtk.ListBox
return row.filename;
}
deactivateActiveItem()
changeActiveRow(rowId)
{
const row = this.get_row_at_index(rowId);
if(!row)
return false;
row.activate();
return true;
}
changeRepeatMode(mode)
{
const lastMode = Object.keys(RepeatMode).length - 1;
const row = this.getActiveRow();
if(!row) return null;
if(mode < 0 || mode > lastMode) {
warn(`ignored invalid repeat mode value: ${mode}`);
return;
}
if(mode >= 0)
this.repeatMode = mode;
else {
this.repeatMode++;
if(this.repeatMode > lastMode)
this.repeatMode = 0;
}
const repeatButton = row.child.get_first_child();
repeatButton.icon_name = repeatIcons[this.repeatMode];
debug(`set repeat mode: ${this.repeatMode}`);
}
_deactivateActiveItem(isRemoveChange)
{
if(this.activeRowId < 0)
return;
@@ -76,26 +147,90 @@ class ClapperPlaylistWidget extends Gtk.ListBox
const row = this.getActiveRow();
if(!row) return null;
const icon = row.child.get_first_child();
const button = row.child.get_last_child();
const repeatButton = row.child.get_first_child();
repeatButton.sensitive = false;
repeatButton.icon_name = 'open-menu-symbolic';
icon.icon_name = 'open-menu-symbolic';
button.icon_name = 'list-remove-symbolic';
if(isRemoveChange) {
const removeButton = row.child.get_last_child();
removeButton.icon_name = 'list-remove-symbolic';
}
}
_switchTrack(isPrevious)
{
const rowId = (isPrevious)
? this.activeRowId - 1
: this.activeRowId + 1;
return this.changeActiveRow(rowId);
}
_onRowActivated(listBox, row)
{
const { player } = this.get_ancestor(Gtk.Grid);
const icon = row.child.get_first_child();
const button = row.child.get_last_child();
const repeatButton = row.child.get_first_child();
const removeButton = row.child.get_last_child();
this.deactivateActiveItem();
icon.icon_name = 'media-playback-start-symbolic';
button.icon_name = 'window-close-symbolic';
this._deactivateActiveItem(true);
repeatButton.sensitive = true;
repeatButton.icon_name = repeatIcons[this.repeatMode];
removeButton.icon_name = 'window-close-symbolic';
this.activeRowId = row.get_index();
player.set_uri(row.uri);
}
_handleStreamEnded(player)
{
/* Seek to beginning when repeating track
* or playlist with only one item */
if(
this.repeatMode === RepeatMode.TRACK
|| (this.repeatMode !== RepeatMode.NONE
&& this.activeRowId === 0
&& !this.get_row_at_index(1))
) {
debug('seeking to beginning');
player.seek(0);
return true;
}
if(this.repeatMode === RepeatMode.SHUFFLE) {
const playlistIds = [];
let index = 0;
debug('selecting random playlist item');
while(this.get_row_at_index(index)) {
/* We prefer to not repeat the same track */
if(index !== this.activeRowId)
playlistIds.push(index);
index++;
}
/* We always have non-empty array here,
* otherwise seek to beginning is performed */
const randomId = playlistIds[
Math.floor(Math.random() * playlistIds.length)
];
debug(`selected random playlist item: ${randomId}`);
return this.changeActiveRow(randomId);
}
if(this.nextTrack())
return true;
if(this.repeatMode === RepeatMode.PLAYLIST)
return this.changeActiveRow(0);
this._deactivateActiveItem(false);
return false;
}
});
let PlaylistItem = GObject.registerClass(
@@ -104,7 +239,6 @@ class ClapperPlaylistItem extends Gtk.ListBoxRow
_init(uri)
{
super._init({
/* TODO: Fix playlist navigation in fullscreen */
can_focus: false,
});
@@ -112,7 +246,7 @@ class ClapperPlaylistItem extends Gtk.ListBoxRow
this.isLocalFile = false;
let filename;
if(Gst.Uri.get_protocol(uri) === 'file') {
if(Misc.getUriProtocol(uri) === 'file') {
filename = GLib.path_get_basename(
GLib.filename_from_uri(uri)[0]
);
@@ -128,9 +262,14 @@ class ClapperPlaylistItem extends Gtk.ListBoxRow
margin_end: 6,
height_request: 22,
});
const icon = new Gtk.Image({
const repeatButton = new Gtk.Button({
icon_name: 'open-menu-symbolic',
sensitive: false,
});
repeatButton.add_css_class('flat');
repeatButton.add_css_class('circular');
repeatButton.add_css_class('popoverbutton');
repeatButton.connect('clicked', this._onRepeatClicked.bind(this));
const label = new Gtk.Label({
label: this.filename,
single_line_mode: true,
@@ -139,17 +278,17 @@ class ClapperPlaylistItem extends Gtk.ListBoxRow
hexpand: true,
halign: Gtk.Align.START,
});
const button = new Gtk.Button({
const removeButton = new Gtk.Button({
icon_name: 'list-remove-symbolic',
});
button.add_css_class('flat');
button.add_css_class('circular');
button.add_css_class('popoverbutton');
button.connect('clicked', this._onRemoveClicked.bind(this));
removeButton.add_css_class('flat');
removeButton.add_css_class('circular');
removeButton.add_css_class('popoverbutton');
removeButton.connect('clicked', this._onRemoveClicked.bind(this));
box.append(icon);
box.append(repeatButton);
box.append(label);
box.append(button);
box.append(removeButton);
this.set_child(box);
/* FIXME: D&D inside popover is broken in GTK4
@@ -172,6 +311,13 @@ class ClapperPlaylistItem extends Gtk.ListBoxRow
*/
}
_onRepeatClicked(button)
{
const listBox = this.get_ancestor(Gtk.ListBox);
listBox.changeRepeatMode();
}
_onRemoveClicked(button)
{
const listBox = this.get_ancestor(Gtk.ListBox);

View File

@@ -41,6 +41,7 @@ class ClapperGeneralPage extends PrefsBase.Grid
comboBox.connect('changed', this._onVolumeInitialChanged.bind(this, spinButton));
this.addTitle('Finish');
this.addCheckButton('Keep showing last frame', 'keep-last-frame');
this.addCheckButton('Close after playback', 'close-auto');
}

View File

@@ -31,12 +31,14 @@ class ClapperWidget extends Gtk.Grid
this.isDragAllowed = false;
this.isSwipePerformed = false;
this.isReleaseKeyEnabled = false;
this.isCursorInPlayer = false;
this.isPopoverOpen = false;
this._hideControlsTimeout = null;
this._updateTimeTimeout = null;
this.surfaceMapSignal = null;
this.needsCursorRestore = false;
@@ -103,18 +105,20 @@ class ClapperWidget extends Gtk.Grid
const dropTarget = this._getDropTarget();
playerWidget.add_controller(dropTarget);
/* Applied only for widget to detect simple action key releases */
const keyController = new Gtk.EventControllerKey();
keyController.connect('key-released', this._onKeyReleased.bind(this));
this.add_controller(keyController);
}
revealControls(isAllowInput)
revealControls()
{
this.revealerTop.revealChild(true);
this.revealerBottom.revealChild(true);
this._checkSetUpdateTimeInterval();
if(isAllowInput)
this.setControlsCanFocus(true);
/* Reset timeout if already revealed, otherwise
* timeout will be set after reveal finishes */
if(this.revealerTop.child_revealed)
@@ -138,13 +142,6 @@ class ClapperWidget extends Gtk.Grid
debug('changing fullscreen mode');
this.isFullscreenMode = isFullscreen;
const root = this.get_root();
const action = (isFullscreen) ? 'add' : 'remove';
root[action + '_css_class']('gpufriendlyfs');
if(!this.isMobileMonitor)
root[action + '_css_class']('tvmode');
if(!isFullscreen)
this._clearTimeout('updateTime');
@@ -157,28 +154,9 @@ class ClapperWidget extends Gtk.Grid
if(this.revealerTop.child_revealed)
this._checkSetUpdateTimeInterval();
this.setControlsCanFocus(false);
if(this.player.playOnFullscreen && isFullscreen) {
this.player.playOnFullscreen = false;
this.player.play();
}
debug(`interface in fullscreen mode: ${isFullscreen}`);
}
setControlsCanFocus(isControlsFocus)
{
this.revealerBottom.can_focus = isControlsFocus;
this.player.widget.can_focus = !isControlsFocus;
const focusWidget = (isControlsFocus)
? this.controls.togglePlayButton
: this.player.widget;
focusWidget.grab_focus();
}
_changeControlsPlacement(isOnTop)
{
if(isOnTop) {
@@ -253,7 +231,11 @@ class ClapperWidget extends Gtk.Grid
break;
case GstClapper.ClapperSubtitleInfo:
type = 'subtitle';
text = info.get_language() || 'Undetermined';
const subsLang = info.get_language();
text = (subsLang) ? subsLang.split(',')[0] : 'Undetermined';
const subsTitle = Misc.getSubsTitle(info.get_title());
if(subsTitle)
text += ', ' + subsTitle;
break;
default:
debug(`unrecognized media info type: ${info.constructor}`);
@@ -286,7 +268,10 @@ class ClapperWidget extends Gtk.Grid
debug(`${type} caps: ${caps.to_string()}`);
}
if(type === 'video') {
const isShowVis = (parsedInfo[`${type}Tracks`].length === 0);
const isShowVis = (
!parsedInfo.videoTracks.length
&& parsedInfo.audioTracks.length
);
this.showVisualizationsButton(isShowVis);
}
if(!parsedInfo[`${type}Tracks`].length) {
@@ -310,17 +295,14 @@ class ClapperWidget extends Gtk.Grid
updateTitle(mediaInfo)
{
let title = mediaInfo.get_title();
let title = this.player.customVideoTitle;
if(!title)
title = this.player.customVideoTitle;
title = mediaInfo.get_title();
if(!title) {
const item = this.player.playlistWidget.getActiveRow();
title = (item.isLocalFile && item.filename.includes('.'))
? item.filename.split('.').slice(0, -1).join('.')
: item.filename;
title = item.filename;
}
this.root.title = title;
@@ -447,10 +429,8 @@ class ClapperWidget extends Gtk.Grid
break;
case GstClapper.ClapperState.STOPPED:
debug('player state changed to: STOPPED');
this.controls.currentPosition = 0;
this.controls.positionScale.set_value(0);
this.controls.setInitialState();
this.revealerTop.showTitle = false;
this.controls.togglePlayButton.setPrimaryIcon();
break;
case GstClapper.ClapperState.PAUSED:
debug('player state changed to: PAUSED');
@@ -470,14 +450,9 @@ class ClapperWidget extends Gtk.Grid
const durationSeconds = duration / Gst.SECOND;
const durationFloor = Math.floor(durationSeconds);
/* Sometimes GstPlayer might re-emit
* duration changed during playback */
if(this.controls.currentDuration === durationFloor)
return;
debug(`duration changed: ${durationSeconds}`);
this.controls.currentDuration = durationFloor;
this.controls.showHours = (durationFloor >= 3600);
this.controls.positionAdjustment.set_upper(durationFloor);
this.controls.durationFormatted = Misc.getFormattedTime(durationFloor);
this.controls.updateElapsedLabel();
@@ -558,30 +533,70 @@ class ClapperWidget extends Gtk.Grid
_onWindowMap(window)
{
const surface = window.get_surface();
const monitor = window.display.get_monitor_at_surface(surface);
const geometry = monitor.geometry;
const size = JSON.parse(settings.get_string('window-size'));
debug(`monitor application-pixels: ${geometry.width}x${geometry.height}`);
if(geometry.width >= size[0] && geometry.height >= size[1]) {
window.set_default_size(size[0], size[1]);
debug(`restored window size: ${size[0]}x${size[1]}`);
}
const monitorWidth = Math.max(geometry.width, geometry.height);
if(monitorWidth < 1280) {
this.isMobileMonitor = true;
debug('mobile monitor detected');
}
if(!surface.mapped)
this.surfaceMapSignal = surface.connect(
'notify::mapped', this._onSurfaceMapNotify.bind(this)
);
else
this._onSurfaceMapNotify(surface);
surface.connect('notify::state', this._onStateNotify.bind(this));
surface.connect('enter-monitor', this._onEnterMonitor.bind(this));
surface.connect('layout', this._onLayoutUpdate.bind(this));
this.player._onWindowMap(window);
}
_onSurfaceMapNotify(surface)
{
if(!surface.mapped)
return;
if(this.surfaceMapSignal) {
surface.disconnect(this.surfaceMapSignal);
this.surfaceMapSignal = null;
}
const monitor = surface.display.get_monitor_at_surface(surface);
const size = JSON.parse(settings.get_string('window-size'));
const hasMonitor = Boolean(monitor && monitor.geometry);
/* Let GTK handle window restore if no monitor, otherwise
check if its size is greater then saved window size */
if(
!hasMonitor
|| (monitor.geometry.width >= size[0]
&& monitor.geometry.height >= size[1])
) {
if(!hasMonitor)
debug('restoring window size without monitor geometry');
this.root.set_default_size(size[0], size[1]);
debug(`restored window size: ${size[0]}x${size[1]}`);
}
}
_onEnterMonitor(surface, monitor)
{
debug('entered new monitor');
const { geometry } = monitor;
debug(`monitor application-pixels: ${geometry.width}x${geometry.height}`);
const monitorWidth = Math.max(geometry.width, geometry.height);
this.isMobileMonitor = (monitorWidth < 1280);
debug(`mobile monitor detected: ${this.isMobileMonitor}`);
const hasTVCss = this.root.has_css_class('tvmode');
if(hasTVCss === this.isMobileMonitor) {
const action = (this.isMobileMonitor) ? 'remove' : 'add';
this.root[action + '_css_class']('tvmode');
}
/* Update top revealer display mode */
this.revealerTop.setFullscreenMode(this.isFullscreenMode, this.isMobileMonitor);
}
_clearTimeout(name)
{
if(!this[`_${name}Timeout`])
@@ -619,7 +634,6 @@ class ClapperWidget extends Gtk.Grid
this.revealerTop.revealChild(false);
this.revealerBottom.revealChild(false);
}
this.setControlsCanFocus(false);
return GLib.SOURCE_REMOVE;
});
@@ -632,6 +646,7 @@ class ClapperWidget extends Gtk.Grid
&& !this.isMobileMonitor
&& !this._updateTimeTimeout
) {
debug('setting update time interval');
this._setUpdateTimeInterval();
}
}
@@ -706,10 +721,11 @@ class ClapperWidget extends Gtk.Grid
_getDropTarget()
{
const dropTarget = new Gtk.DropTarget({
actions: Gdk.DragAction.COPY,
actions: Gdk.DragAction.COPY | Gdk.DragAction.MOVE,
preload: true,
});
dropTarget.set_gtypes([GObject.TYPE_STRING]);
dropTarget.connect('motion', this._onDataMotion.bind(this));
dropTarget.connect('drop', this._onDataDrop.bind(this));
dropTarget.connect('notify::value', this._onDropValueNotify.bind(this));
@@ -766,6 +782,28 @@ class ClapperWidget extends Gtk.Grid
}
}
_onKeyReleased(controller, keyval, keycode, state)
{
/* Ignore releases that did not trigger keypress
* e.g. while holding left "Super" key */
if(!this.isReleaseKeyEnabled)
return;
switch(keyval) {
case Gdk.KEY_Right:
case Gdk.KEY_Left:
const value = Math.round(
this.controls.positionScale.get_value()
);
this.player.seek_seconds(value);
this._setHideControlsTimeout();
this.isReleaseKeyEnabled = false;
break;
default:
break;
}
}
_onDragUpdate(gesture, offsetX, offsetY)
{
if(!this.isDragAllowed || this.isFullscreenMode)
@@ -915,6 +953,11 @@ class ClapperWidget extends Gtk.Grid
ytClient.getVideoInfoPromise(videoId).catch(debug);
}
_onDataMotion(dropTarget, x, y)
{
return Gdk.DragAction.MOVE;
}
_onDataDrop(dropTarget, value, x, y)
{
const files = value.split(/\r?\n/).filter(uri => {
@@ -927,7 +970,9 @@ class ClapperWidget extends Gtk.Grid
for(let index in files)
files[index] = Gio.File.new_for_uri(files[index]);
this.root.application.open(files, "");
const app = this.root.application;
app.isFileAppend = Boolean(dropTarget.drop.actions & Gdk.DragAction.COPY);
app.open(files, "");
return true;
}

View File

@@ -27,7 +27,7 @@ class ClapperWidgetRemote extends Gtk.Grid
this.togglePlayButton.remove_css_class('flat');
this.togglePlayButton.child.add_css_class('playbackicon');
this.togglePlayButton.connect(
'clicked', this.sendWs.bind(this, 'toggle_play')
'clicked', () => this.sendWs('toggle_play')
);
this.attach(this.togglePlayButton, 0, 0, 1, 1);

View File

@@ -38,7 +38,7 @@ var YouTubeClient = GObject.registerClass({
this.lastInfo = null;
this.postInfo = {
clientVersion: null,
clientVersion: "2.20210605.09.00",
visitorData: "",
};
@@ -729,6 +729,7 @@ var YouTubeClient = GObject.registerClass({
return new Promise((resolve, reject) => {
const query = [
`video_id=${videoId}`,
`html5=1`,
`el=embedded`,
`eurl=https://youtube.googleapis.com/v/${videoId}`,
`sts=${this.cachedSig.timestamp}`,

View File

@@ -4,11 +4,11 @@
<section>
<item>
<attribute name="label" translatable="yes">Open Local...</attribute>
<attribute name="action">app.openLocal</attribute>
<attribute name="action">app.open_local</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Open URI...</attribute>
<attribute name="action">app.openUri</attribute>
<attribute name="action">app.open_uri</attribute>
</item>
</section>
<section>
@@ -16,14 +16,12 @@
<attribute name="label" translatable="yes">Preferences</attribute>
<attribute name="action">app.prefs</attribute>
</item>
</section>
<section>
<!--
<item>
<attribute name="label" translatable="yes">Keyboard Shortcuts</attribute>
<attribute name="label" translatable="yes">Shortcuts</attribute>
<attribute name="action">app.shortcuts</attribute>
</item>
-->
</section>
<section>
<item>
<attribute name="label" translatable="yes">About Clapper</attribute>
<attribute name="action">app.about</attribute>

133
ui/help-overlay.ui Normal file
View File

@@ -0,0 +1,133 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<object class="GtkShortcutsWindow" id="help_overlay">
<property name="modal">True</property>
<child>
<object class="GtkShortcutsSection">
<property name="section-name">app</property>
<child>
<object class="GtkShortcutsGroup">
<property name="title" translatable="yes">General</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Show shortcuts</property>
<property name="accelerator">F1 &lt;Ctrl&gt;question</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Toggle fullscreen</property>
<property name="accelerator">F11 f</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Reveal OSD (fullscreen only)</property>
<property name="accelerator">Return</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Quit</property>
<property name="accelerator">&lt;Ctrl&gt;Q Q</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkShortcutsGroup">
<property name="title" translatable="yes">Media</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Open Local</property>
<property name="accelerator">&lt;Ctrl&gt;O</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Open URI</property>
<property name="accelerator">&lt;Ctrl&gt;U</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkShortcutsGroup">
<property name="title" translatable="yes">Playlist</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Next item</property>
<property name="accelerator">&lt;Ctrl&gt;Right</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Previous item</property>
<property name="accelerator">&lt;Ctrl&gt;Left</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Change repeat mode</property>
<property name="accelerator">&lt;Ctrl&gt;R</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Export to file</property>
<property name="accelerator">&lt;Ctrl&gt;E</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkShortcutsGroup">
<property name="title" translatable="yes">Playback</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Toggle play</property>
<property name="accelerator">space</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Volume up</property>
<property name="accelerator">Up</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Volume down</property>
<property name="accelerator">Down</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Seek forward</property>
<property name="accelerator">Right</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Seek backward</property>
<property name="accelerator">Left</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Next chapter</property>
<property name="accelerator">&lt;Shift&gt;Right</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes">Previous chapter</property>
<property name="accelerator">&lt;Shift&gt;Left</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>