97 Commits

Author SHA1 Message Date
Rafostar
f64f438f1e API: Add playlist support 2021-06-06 19:54:56 +02:00
Rafał Dzięgiel
8fc64eaf73 sink: Automatically draw black when going to NULL state
Remove all workarounds including ignore_textures prop and draw black functions and handle that automatically in sink when going into NULL state.
2021-06-02 20:56:50 +02:00
Rafał Dzięgiel
0b7f31b7c2 API: Properly return a boolean instead of a number 2021-06-02 20:24:40 +02:00
Rafał Dzięgiel
2d4353aaec Append some common subtitle track titles
In order to not end up with multiple subitle tracks simply named for e.g. "English", add some common postfix to it when detected in track title.
2021-06-02 15:47:09 +02:00
Rafał Dzięgiel
7062af472b API: Add function to get subtitle track title 2021-06-02 11:08:30 +02:00
Rafał Dzięgiel
1f4698448a Detect used GStreamer plugin names
Allows seeing what plugins are used with GST_DEBUG=Clapper:4. This is also needed for yet to come functionality of setting elements props.
2021-06-01 21:57:14 +02:00
Rafał Dzięgiel
95c8316af6 Change Enter key help description
OSD makes more sense to users then controls as it also shows top title and time layers
2021-06-01 08:31:02 +02:00
Rafał Dzięgiel
06d9f302c2 Revert "sink: Use g_main_context_invoke_full for drawing"
Not much benefit from using this function and unlike g_idle_add_full it
does not allow to skip a frame when previous one was not finished (slow HW).

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

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

We cannot detect surface width during app widgets assembly, so update the controls
revealers state on first surface update after window is mapped and only if running
on mobile width. Otherwise do not do anything like before which will result in
showing fully revealed controls (default).
2021-04-15 15:27:28 +02:00
Rafał Dzięgiel
ca7b44092e API: do not lock when changing scaled size values
Those values are private and should be accessed only from GTK thread, so locking widget should not be necessary here.
2021-04-15 11:58:12 +02:00
Rafał Dzięgiel
adbcfecb5e API: unset needs_info_update when stopped 2021-04-15 11:30:55 +02:00
Rafał Dzięgiel
a717e481e8 Fix missing top left menu buttons. Fixes #66
On some non-default system configurations the "menu" layout item might be replaced with one named "icon". Handle "icon" the same as "menu" when organizing headerbar buttons.
2021-04-14 17:48:57 +02:00
93 changed files with 3886 additions and 2315 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:
@@ -62,8 +63,7 @@ It can be enabled from inside player preferences dialog inside `Advanced -> GStr
Since the whole app is rendered using your GPU, users of VERY weak GPUs might want to disable the "render window shadows" option to have more GPU power available for non-fullscreen video rendering.
## Other Questions?
Feel free to ask me any questions.<br>
Use either GitHub [discussions](https://github.com/Rafostar/clapper/discussions) or come and talk on Matrix: **#clapper-player:matrix.org**
Feel free to ask me any questions. Come and talk on Matrix: [#clapper-player:matrix.org](https://matrix.to/#/#clapper-player:matrix.org)
## Special Thanks
Many thanks to [sp1ritCS](https://github.com/sp1ritCS) for creating and maintaining package build files.

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

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

@@ -102,7 +102,11 @@
<!-- YouTube -->
<key name="yt-adaptive-enabled" type="b">
<default>false</default>
<summary>Enable to use adaptive streaming</summary>
<summary>Enable to use adaptive streaming for YouTube</summary>
</key>
<key name="yt-quality-type" type="s">
<default>"hfr"</default>
<summary>Max YouTube video quality type</summary>
</key>
<!-- Other -->

View File

@@ -52,6 +52,24 @@
</screenshot>
</screenshots>
<releases>
<release version="0.2.1" date="2021-04-19">
<description>
<p>Player:</p>
<ul>
<li>Fix missing top left menu buttons on some system configurations</li>
<li>Fix potential video sink deadlock</li>
<li>Do not show mobile controls transition on launch</li>
<li>Show tooltip with full playlist item text on hover</li>
</ul>
<p>YouTube:</p>
<ul>
<li>Auto select best matching resolution for used monitor</li>
<li>Added some YouTube related preferences</li>
<li>Added support for live HLS videos</li>
<li>Added support for non-adaptive live HLS streaming</li>
</ul>
</description>
</release>
<release version="0.2.0" date="2021-04-13">
<description>
<p>New features:</p>

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

@@ -28,6 +28,9 @@
#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-playlist.h>
#include <gst/clapper/gstclapper-playlist-item.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,788 @@
/*
* 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);
GstClapperPlaylist *playlist;
GstClapperPlaylistItem *item;
GST_DEBUG ("Handle OpenUri");
playlist = gst_clapper_playlist_new ();
item = gst_clapper_playlist_item_new (uri);
gst_clapper_playlist_append (playlist, item);
gst_clapper_set_playlist (clapper, playlist);
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)
{
GstClapperMpris *self;
self = g_object_new (GST_TYPE_CLAPPER,
"own-name", own_name, "id_path", id_path,
"identity", identity, "desktop-entry", desktop_entry,
"default-art-url", default_art_url, NULL);
return self;
}

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

@@ -0,0 +1,49 @@
/*
* 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_PLAYLIST_ITEM_PRIVATE_H__
#define __GST_CLAPPER_PLAYLIST_ITEM_PRIVATE_H__
#include "gstclapper-playlist.h"
struct _GstClapperPlaylistItem
{
GstObject parent;
/* ID of the playlist this item belongs to */
gchar *owner_uuid;
gint id;
gchar *uri;
gchar *suburi;
gchar *custom_title;
/* Signals */
gulong activated_signal_id;
};
struct _GstClapperPlaylistItemClass
{
GstObjectClass parent_class;
};
G_GNUC_INTERNAL
void gst_clapper_playlist_item_mark_added (GstClapperPlaylistItem *item, GstClapperPlaylist *playlist);
#endif /* __GST_CLAPPER_PLAYLIST_ITEM_PRIVATE_H__ */

View File

@@ -0,0 +1,281 @@
/*
* 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-playlist-item.h"
#include "gstclapper-playlist-item-private.h"
#include "gstclapper-playlist-private.h"
enum
{
PROP_0,
PROP_URI,
PROP_SUBURI,
PROP_CUSTOM_TITLE,
PROP_LAST
};
enum
{
SIGNAL_ACTIVATED,
SIGNAL_LAST
};
#define parent_class gst_clapper_playlist_item_parent_class
G_DEFINE_TYPE (GstClapperPlaylistItem, gst_clapper_playlist_item, GST_TYPE_OBJECT);
static guint signals[SIGNAL_LAST] = { 0, };
static GParamSpec *param_specs[PROP_LAST] = { NULL, };
static void gst_clapper_playlist_item_set_property (GObject * object,
guint prop_id, const GValue * value, GParamSpec * pspec);
static void gst_clapper_playlist_item_get_property (GObject * object,
guint prop_id, GValue * value, GParamSpec * pspec);
static void gst_clapper_playlist_item_dispose (GObject * object);
static void gst_clapper_playlist_item_finalize (GObject * object);
static void
gst_clapper_playlist_item_init (GstClapperPlaylistItem * self)
{
self->owner_uuid = NULL;
self->id = -1;
self->uri = NULL;
self->suburi = NULL;
self->custom_title = NULL;
}
static void
gst_clapper_playlist_item_class_init (GstClapperPlaylistItemClass * klass)
{
GObjectClass *gobject_class = (GObjectClass *) klass;
gobject_class->set_property = gst_clapper_playlist_item_set_property;
gobject_class->get_property = gst_clapper_playlist_item_get_property;
gobject_class->dispose = gst_clapper_playlist_item_dispose;
gobject_class->finalize = gst_clapper_playlist_item_finalize;
param_specs[PROP_URI] = g_param_spec_string ("uri",
"URI", "Playlist Item URI", NULL,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
param_specs[PROP_SUBURI] = g_param_spec_string ("suburi",
"Subtitle URI", "Playlist Item Subtitle URI", NULL,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
param_specs[PROP_CUSTOM_TITLE] = g_param_spec_string ("custom-title",
"Custom Title", "Playlist Item Custom Title", NULL,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
g_object_class_install_properties (gobject_class, PROP_LAST, param_specs);
signals[SIGNAL_ACTIVATED] =
g_signal_new ("activated", G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, 0, NULL,
NULL, NULL, G_TYPE_NONE, 0, G_TYPE_INVALID);
}
static void
gst_clapper_playlist_item_set_property (GObject * object,
guint prop_id, const GValue * value, GParamSpec * pspec)
{
GstClapperPlaylistItem *self = GST_CLAPPER_PLAYLIST_ITEM (object);
switch (prop_id) {
case PROP_URI:
self->uri = g_value_dup_string (value);
break;
case PROP_SUBURI:
g_free (self->suburi);
self->suburi = g_value_dup_string (value);
break;
case PROP_CUSTOM_TITLE:
self->custom_title = g_value_dup_string (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gst_clapper_playlist_item_get_property (GObject * object,
guint prop_id, GValue * value, GParamSpec * pspec)
{
GstClapperPlaylistItem *self = GST_CLAPPER_PLAYLIST_ITEM (object);
switch (prop_id) {
case PROP_URI:
g_value_set_string (value, self->uri);
break;
case PROP_SUBURI:
g_value_set_string (value, self->suburi);
break;
case PROP_CUSTOM_TITLE:
g_value_set_string (value, self->custom_title);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gst_clapper_playlist_item_dispose (GObject * object)
{
GstClapperPlaylistItem *self = GST_CLAPPER_PLAYLIST_ITEM (object);
if (self->activated_signal_id) {
g_signal_handler_disconnect (self, self->activated_signal_id);
self->activated_signal_id = 0;
}
G_OBJECT_CLASS (parent_class)->dispose (object);
}
static void
gst_clapper_playlist_item_finalize (GObject * object)
{
GstClapperPlaylistItem *self = GST_CLAPPER_PLAYLIST_ITEM (object);
g_free (self->owner_uuid);
g_free (self->uri);
g_free (self->suburi);
g_free (self->custom_title);
G_OBJECT_CLASS (parent_class)->finalize (object);
}
static void
item_activate_cb (GstClapperPlaylistItem * self, GParamSpec * pspec,
GstClapperPlaylist * playlist)
{
gst_clapper_playlist_emit_item_activated (playlist, self);
}
void
gst_clapper_playlist_item_mark_added (GstClapperPlaylistItem * self,
GstClapperPlaylist * playlist)
{
GST_OBJECT_LOCK (self);
self->owner_uuid = g_strdup (playlist->uuid);
self->id = playlist->id_count;
self->activated_signal_id = g_signal_connect (self, "activated",
G_CALLBACK (item_activate_cb), playlist);
GST_OBJECT_UNLOCK (self);
}
/**
* gst_clapper_playlist_item_new:
*
* Creates a new #GstClapperPlaylistItem.
*
* Returns: (transfer full): a new #GstClapperPlaylistItem object.
*/
GstClapperPlaylistItem *
gst_clapper_playlist_item_new (const gchar * uri)
{
return g_object_new (GST_TYPE_CLAPPER_PLAYLIST_ITEM, "uri", uri, NULL);
}
/**
* gst_clapper_playlist_item_new_titled:
* @uri: An URI pointing to media
* @custom_title: A custom title for this item
*
* Creates a new #GstClapperPlaylistItem with a custom title.
*
* Normally item title is obtained from media info or local filename,
* use this function for online sources where media title cannot be
* determined or if you want to override original title for some reason.
*
* Returns: (transfer full): a new #GstClapperPlaylistItem object.
*/
GstClapperPlaylistItem *
gst_clapper_playlist_item_new_titled (const gchar * uri,
const gchar * custom_title)
{
return g_object_new (GST_TYPE_CLAPPER_PLAYLIST_ITEM, "uri", uri,
"custom_title", custom_title, NULL);
}
/**
* gst_clapper_playlist_item_copy:
* @item: #GstClapperPlaylistItem
*
* Duplicates a #GstClapperPlaylistItem.
*
* Duplicated items do not belong to any playlist.
* Use this function if you want to append the same
* media into another #GstClapperPlaylist instance.
*
* Returns: (transfer full): a new #GstClapperPlaylistItem object.
*/
GstClapperPlaylistItem *
gst_clapper_playlist_item_copy (GstClapperPlaylistItem * source)
{
g_return_val_if_fail (GST_IS_CLAPPER_PLAYLIST_ITEM (source), NULL);
return g_object_new (GST_TYPE_CLAPPER_PLAYLIST_ITEM, "uri", source->uri,
"suburi", source->suburi, "custom-title", source->custom_title, NULL);
}
/**
* gst_clapper_playlist_item_set_suburi:
* @item: #GstClapperPlaylistItem
* @suburi: subtitle URI
*
* Sets the external subtitle URI.
*/
void
gst_clapper_playlist_item_set_suburi (GstClapperPlaylistItem * self,
const gchar * suburi)
{
/* TODO: When setting this property for an item that is currently active,
* it should be combined with a call to
* gst_clapper_set_subtitle_track_enabled(Clapper, TRUE),
* so the subtitles are actually rendered.
*/
g_return_if_fail (GST_IS_CLAPPER_PLAYLIST_ITEM (self));
g_object_set (self, "suburi", suburi, NULL);
}
/**
* gst_clapper_playlist_item_activate:
* @item: #GstClapperPlaylistItem
*
* Activates the #GstClapperPlaylistItem.
*/
void
gst_clapper_playlist_item_activate (GstClapperPlaylistItem * self)
{
g_return_if_fail (GST_IS_CLAPPER_PLAYLIST_ITEM (self));
g_signal_emit (self, signals[SIGNAL_ACTIVATED], 0);
}

View File

@@ -0,0 +1,62 @@
/*
* 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_PLAYLIST_ITEM_H__
#define __GST_CLAPPER_PLAYLIST_ITEM_H__
#include <gst/clapper/clapper-prelude.h>
G_BEGIN_DECLS
typedef struct _GstClapperPlaylistItem GstClapperPlaylistItem;
typedef struct _GstClapperPlaylistItemClass GstClapperPlaylistItemClass;
#define GST_TYPE_CLAPPER_PLAYLIST_ITEM (gst_clapper_playlist_item_get_type ())
#define GST_IS_CLAPPER_PLAYLIST_ITEM(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_CLAPPER_PLAYLIST_ITEM))
#define GST_IS_CLAPPER_PLAYLIST_ITEM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_CLAPPER_PLAYLIST_ITEM))
#define GST_CLAPPER_PLAYLIST_ITEM_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_CLAPPER_PLAYLIST_ITEM, GstClapperPlaylistItemClass))
#define GST_CLAPPER_PLAYLIST_ITEM(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_CLAPPER_PLAYLIST_ITEM, GstClapperPlaylistItem))
#define GST_CLAPPER_PLAYLIST_ITEM_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_CLAPPER_PLAYLIST_ITEM, GstClapperPlaylistItemClass))
#define GST_CLAPPER_PLAYLIST_ITEM_CAST(obj) ((GstClapperPlaylistItem*)(obj))
#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC
G_DEFINE_AUTOPTR_CLEANUP_FUNC(GstClapperPlaylistItem, gst_object_unref)
#endif
GST_CLAPPER_API
GType gst_clapper_playlist_item_get_type (void);
GST_CLAPPER_API
GstClapperPlaylistItem * gst_clapper_playlist_item_new (const gchar *uri);
GST_CLAPPER_API
GstClapperPlaylistItem * gst_clapper_playlist_item_new_titled (const gchar *uri, const gchar *custom_title);
GST_CLAPPER_API
GstClapperPlaylistItem * gst_clapper_playlist_item_copy (GstClapperPlaylistItem *item);
GST_CLAPPER_API
void gst_clapper_playlist_item_set_suburi (GstClapperPlaylistItem *item, const gchar *suburi);
GST_CLAPPER_API
void gst_clapper_playlist_item_activate (GstClapperPlaylistItem *item);
G_END_DECLS
#endif /* __GST_CLAPPER_PLAYLIST_ITEM_H__ */

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_PLAYLIST_PRIVATE_H__
#define __GST_CLAPPER_PLAYLIST_PRIVATE_H__
#include "gstclapper-playlist.h"
struct _GstClapperPlaylist
{
GstObject parent;
gchar *uuid;
gint id_count;
GArray *items;
gint active_index;
};
struct _GstClapperPlaylistClass
{
GstObjectClass parent_class;
};
G_GNUC_INTERNAL
void gst_clapper_playlist_emit_item_activated (GstClapperPlaylist *playlist, GstClapperPlaylistItem *item);
#endif /* __GST_CLAPPER_PLAYLIST_PRIVATE_H__ */

275
lib/gst/clapper/gstclapper-playlist.c vendored Normal file
View File

@@ -0,0 +1,275 @@
/*
* 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-playlist.h"
#include "gstclapper-playlist-private.h"
#include "gstclapper-playlist-item.h"
#include "gstclapper-playlist-item-private.h"
enum
{
SIGNAL_ITEM_ACTIVATED,
SIGNAL_LAST
};
#define parent_class gst_clapper_playlist_parent_class
G_DEFINE_TYPE (GstClapperPlaylist, gst_clapper_playlist, GST_TYPE_OBJECT);
static guint signals[SIGNAL_LAST] = { 0, };
static void gst_clapper_playlist_dispose (GObject * object);
static void gst_clapper_playlist_finalize (GObject * object);
static void
gst_clapper_playlist_init (GstClapperPlaylist * self)
{
self->uuid = g_uuid_string_random ();
self->id_count = 0;
self->items = g_array_new (FALSE, FALSE, sizeof (GstClapperPlaylistItem));
self->active_index = -1;
g_array_set_clear_func (self->items, (GDestroyNotify) gst_object_unref);
}
static void
gst_clapper_playlist_class_init (GstClapperPlaylistClass * klass)
{
GObjectClass *gobject_class = (GObjectClass *) klass;
gobject_class->dispose = gst_clapper_playlist_dispose;
gobject_class->finalize = gst_clapper_playlist_finalize;
signals[SIGNAL_ITEM_ACTIVATED] =
g_signal_new ("item-activated", G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS, 0, NULL,
NULL, NULL, G_TYPE_NONE, 1, GST_TYPE_CLAPPER_PLAYLIST_ITEM);
}
static void
gst_clapper_playlist_dispose (GObject * object)
{
GstClapperPlaylist *self = GST_CLAPPER_PLAYLIST (object);
/* FIXME: Need this for something? */
G_OBJECT_CLASS (parent_class)->dispose (object);
}
static void
gst_clapper_playlist_finalize (GObject * object)
{
GstClapperPlaylist *self = GST_CLAPPER_PLAYLIST (object);
g_free (self->uuid);
g_array_unref (self->items);
G_OBJECT_CLASS (parent_class)->finalize (object);
}
void
gst_clapper_playlist_emit_item_activated (GstClapperPlaylist * self,
GstClapperPlaylistItem * item)
{
g_signal_emit (self, signals[SIGNAL_ITEM_ACTIVATED], 0, item);
}
/**
* gst_clapper_playlist_new:
*
* Creates a new #GstClapperPlaylist.
*
* Returns: (transfer full): a new #GstClapperPlaylist instance.
*/
GstClapperPlaylist *
gst_clapper_playlist_new (void)
{
return g_object_new (GST_TYPE_CLAPPER_PLAYLIST, NULL);
}
/**
* gst_clapper_playlist_append:
* @playlist: #GstClapperPlaylist
* @item: #GstClapperPlaylistItem to append
*
* Adds a new #GstClapperPlaylistItem to the end of playlist.
*
* Returns: %TRUE if the item was added successfully.
*/
gboolean
gst_clapper_playlist_append (GstClapperPlaylist * self, GstClapperPlaylistItem * item)
{
gboolean added = FALSE;
g_return_val_if_fail (GST_IS_CLAPPER_PLAYLIST (self), FALSE);
g_return_val_if_fail (GST_IS_CLAPPER_PLAYLIST_ITEM (item), FALSE);
g_return_val_if_fail (item->owner_uuid == NULL, FALSE);
GST_OBJECT_LOCK (self);
added = g_array_append_val (self->items, item) != NULL;
if (added) {
gst_clapper_playlist_item_mark_added (item, self);
self->id_count++;
}
GST_OBJECT_UNLOCK (self);
return added;
}
/**
* gst_clapper_playlist_get_length:
* @playlist: #GstClapperPlaylist
*
* Returns: Amount of items in playlist.
*/
guint
gst_clapper_playlist_get_length (GstClapperPlaylist * self)
{
guint len;
g_return_val_if_fail (GST_IS_CLAPPER_PLAYLIST (self), 0);
GST_OBJECT_LOCK (self);
len = self->items->len;
GST_OBJECT_UNLOCK (self);
return len;
}
/**
* gst_clapper_playlist_get_item_at_index:
* @playlist: #GstClapperPlaylist
*
* Returns: (transfer none): A #GstClapperPlaylistItem at given index.
*/
GstClapperPlaylistItem *
gst_clapper_playlist_get_item_at_index (GstClapperPlaylist * self, gint index)
{
GstClapperPlaylistItem *item = NULL;
g_return_val_if_fail (GST_IS_CLAPPER_PLAYLIST (self), NULL);
GST_OBJECT_LOCK (self);
if (index < self->items->len)
goto out;
item = &g_array_index (self->items, GstClapperPlaylistItem, index);
out:
GST_OBJECT_UNLOCK (self);
return item;
}
/**
* gst_clapper_playlist_get_active_item:
* @playlist: #GstClapperPlaylist
*
* Returns: (transfer none): A #GstClapperPlaylistItem that is
* currently playing.
*/
GstClapperPlaylistItem *
gst_clapper_playlist_get_active_item (GstClapperPlaylist * self)
{
gint active_index;
GST_OBJECT_LOCK (self);
active_index = self->active_index;
GST_OBJECT_UNLOCK (self);
return gst_clapper_playlist_get_item_at_index (self, active_index);
}
/**
* gst_clapper_playlist_remove_item_at_index:
* @playlist: #GstClapperPlaylist
* @index: Index of #GstClapperPlaylistItem to remove
*
* Removes item at given index from playlist.
*
* Returns: %TRUE if the item was removed successfully.
*/
gboolean
gst_clapper_playlist_remove_item_at_index (GstClapperPlaylist * self, guint index)
{
gboolean removed = FALSE;
g_return_val_if_fail (GST_IS_CLAPPER_PLAYLIST (self), FALSE);
GST_OBJECT_LOCK (self);
if (index >= self->items->len || index == self->active_index)
goto out;
removed = g_array_remove_index (self->items, index) != NULL;
out:
GST_OBJECT_UNLOCK (self);
return removed;
}
/**
* gst_clapper_playlist_remove_item:
* @playlist: #GstClapperPlaylist
* @item: #GstClapperPlaylistItem object to remove
*
* Removes given playlist item from playlist.
*
* Returns: %TRUE if the item was removed successfully.
*/
gboolean
gst_clapper_playlist_remove_item (GstClapperPlaylist * self,
GstClapperPlaylistItem * item)
{
gint i;
gboolean removed = FALSE;
g_return_val_if_fail (GST_IS_CLAPPER_PLAYLIST (self), FALSE);
g_return_val_if_fail (GST_IS_CLAPPER_PLAYLIST_ITEM (item), FALSE);
GST_OBJECT_LOCK (self);
if (strcmp (self->uuid, item->owner_uuid) != 0)
goto out;
for (i = 0; i < self->items->len; i++) {
GstClapperPlaylistItem *curr_item;
curr_item = &g_array_index (self->items, GstClapperPlaylistItem, i);
if (!curr_item)
goto out;
if (item->id == curr_item->id) {
removed = g_array_remove_index (self->items, i) != NULL;
break;
}
}
out:
GST_OBJECT_UNLOCK (self);
return removed;
}

71
lib/gst/clapper/gstclapper-playlist.h vendored Normal file
View File

@@ -0,0 +1,71 @@
/*
* 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_PLAYLIST_H__
#define __GST_CLAPPER_PLAYLIST_H__
#include <gst/clapper/clapper-prelude.h>
#include <gst/clapper/gstclapper-playlist-item.h>
G_BEGIN_DECLS
typedef struct _GstClapperPlaylist GstClapperPlaylist;
typedef struct _GstClapperPlaylistClass GstClapperPlaylistClass;
#define GST_TYPE_CLAPPER_PLAYLIST (gst_clapper_playlist_get_type ())
#define GST_IS_CLAPPER_PLAYLIST(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_TYPE_CLAPPER_PLAYLIST))
#define GST_IS_CLAPPER_PLAYLIST_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_TYPE_CLAPPER_PLAYLIST))
#define GST_CLAPPER_PLAYLIST_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_TYPE_CLAPPER_PLAYLIST, GstClapperPlaylistClass))
#define GST_CLAPPER_PLAYLIST(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_TYPE_CLAPPER_PLAYLIST, GstClapperPlaylist))
#define GST_CLAPPER_PLAYLIST_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GST_TYPE_CLAPPER_PLAYLIST, GstClapperPlaylistClass))
#define GST_CLAPPER_PLAYLIST_CAST(obj) ((GstClapperPlaylist*)(obj))
#ifdef G_DEFINE_AUTOPTR_CLEANUP_FUNC
G_DEFINE_AUTOPTR_CLEANUP_FUNC(GstClapperPlaylist, g_object_unref)
#endif
GST_CLAPPER_API
GType gst_clapper_playlist_get_type (void);
GST_CLAPPER_API
GstClapperPlaylist * gst_clapper_playlist_new (void);
GST_CLAPPER_API
gboolean gst_clapper_playlist_append (GstClapperPlaylist *playlist, GstClapperPlaylistItem *item);
GST_CLAPPER_API
guint gst_clapper_playlist_get_length (GstClapperPlaylist *playlist);
GST_CLAPPER_API
GstClapperPlaylistItem *
gst_clapper_playlist_get_item_at_index (GstClapperPlaylist *playlist, gint index);
GST_CLAPPER_API
GstClapperPlaylistItem *
gst_clapper_playlist_get_active_item (GstClapperPlaylist *playlist);
GST_CLAPPER_API
gboolean gst_clapper_playlist_remove_item_at_index (GstClapperPlaylist *playlist, guint index);
GST_CLAPPER_API
gboolean gst_clapper_playlist_remove_item (GstClapperPlaylist *playlist, GstClapperPlaylistItem *item);
G_END_DECLS
#endif /* __GST_CLAPPER_PLAYLIST_H__ */

File diff suppressed because it is too large Load Diff

View File

@@ -30,6 +30,9 @@
#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-playlist.h>
#include <gst/clapper/gstclapper-playlist-item.h>
#include <gst/clapper/gstclapper-mpris.h>
G_BEGIN_DECLS
@@ -153,7 +156,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 +165,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);
@@ -181,16 +195,7 @@ GST_CLAPPER_API
gdouble gst_clapper_get_rate (GstClapper *clapper);
GST_CLAPPER_API
gchar * gst_clapper_get_uri (GstClapper *clapper);
GST_CLAPPER_API
void gst_clapper_set_uri (GstClapper *clapper, const gchar *uri);
GST_CLAPPER_API
gchar * gst_clapper_get_subtitle_uri (GstClapper *clapper);
GST_CLAPPER_API
void gst_clapper_set_subtitle_uri (GstClapper *clapper, const gchar *uri);
void gst_clapper_set_playlist (GstClapper *clapper, GstClapperPlaylist *playlist);
GST_CLAPPER_API
GstClockTime gst_clapper_get_position (GstClapper *clapper);
@@ -213,6 +218,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

@@ -40,7 +40,6 @@ GST_DEBUG_CATEGORY (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",
@@ -86,7 +85,6 @@ enum
PROP_WIDGET,
PROP_FORCE_ASPECT_RATIO,
PROP_PIXEL_ASPECT_RATIO,
PROP_IGNORE_TEXTURES,
};
#define gst_clapper_gl_sink_parent_class parent_class
@@ -133,11 +131,6 @@ gst_clapper_gl_sink_class_init (GstClapperGLSinkClass * klass)
"The pixel aspect ratio of the device", DEFAULT_PAR_N, DEFAULT_PAR_D,
G_MAXINT, 1, 1, 1, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (gobject_class, PROP_IGNORE_TEXTURES,
g_param_spec_boolean ("ignore-textures", "Ignore Textures",
"When enabled, textures will be ignored and not drawn",
DEFAULT_IGNORE_TEXTURES, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
gobject_class->finalize = gst_clapper_gl_sink_finalize;
gstelement_class->change_state = gst_clapper_gl_sink_change_state;
@@ -173,7 +166,6 @@ gst_clapper_gl_sink_init (GstClapperGLSink * clapper_sink)
clapper_sink->force_aspect_ratio = DEFAULT_FORCE_ASPECT_RATIO;
clapper_sink->par_n = DEFAULT_PAR_N;
clapper_sink->par_d = DEFAULT_PAR_D;
clapper_sink->ignore_textures = DEFAULT_IGNORE_TEXTURES;
}
static void
@@ -242,9 +234,6 @@ gst_clapper_gl_sink_get_widget (GstClapperGLSink * clapper_sink)
clapper_sink->bind_pixel_aspect_ratio =
g_object_bind_property (clapper_sink, "pixel-aspect-ratio", clapper_sink->widget,
"pixel-aspect-ratio", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
clapper_sink->bind_ignore_textures =
g_object_bind_property (clapper_sink, "ignore-textures", clapper_sink->widget,
"ignore-textures", G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);
/* Take the floating ref, other wise the destruction of the container will
* make this widget disappear possibly before we are done. */
@@ -290,9 +279,6 @@ 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);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@@ -313,9 +299,6 @@ 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);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@@ -604,8 +587,16 @@ 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);
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 +610,15 @@ 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 = TRUE;
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)

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>
@@ -55,7 +60,6 @@ GST_DEBUG_CATEGORY (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
{
@@ -95,7 +99,6 @@ enum
PROP_0,
PROP_FORCE_ASPECT_RATIO,
PROP_PIXEL_ASPECT_RATIO,
PROP_IGNORE_TEXTURES,
};
static void
@@ -151,13 +154,9 @@ gtk_clapper_gl_widget_size_allocate (GtkWidget * widget,
GtkClapperGLWidget *clapper_widget = GTK_CLAPPER_GL_WIDGET (widget);
gint scale_factor = gtk_widget_get_scale_factor (widget);
GTK_CLAPPER_GL_WIDGET_LOCK (clapper_widget);
clapper_widget->scaled_width = width * scale_factor;
clapper_widget->scaled_height = height * scale_factor;
GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_widget);
gtk_gl_area_queue_render (GTK_GL_AREA (widget));
}
@@ -175,9 +174,6 @@ 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);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@@ -197,9 +193,6 @@ 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);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
@@ -452,10 +445,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",
@@ -467,6 +463,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,11 +569,11 @@ gtk_clapper_gl_widget_render (GtkGLArea * widget, GdkGLContext * context)
GtkClapperGLWidgetPrivate *priv = clapper_widget->priv;
const GstGLFuncs *gl;
GTK_CLAPPER_GL_WIDGET_LOCK (widget);
GTK_CLAPPER_GL_WIDGET_LOCK (clapper_widget);
/* Draw black with GDK context when priv is not available yet.
GTK calls render with GDK context already active. */
if (!priv->context || !priv->other_context || clapper_widget->ignore_textures) {
if (!priv->context || !priv->other_context || clapper_widget->ignore_buffers) {
_draw_black_with_gdk (context);
goto done;
}
@@ -672,7 +675,7 @@ done:
if (priv->other_context)
gst_gl_context_activate (priv->other_context, FALSE);
GTK_CLAPPER_GL_WIDGET_UNLOCK (widget);
GTK_CLAPPER_GL_WIDGET_UNLOCK (clapper_widget);
return FALSE;
}
@@ -849,27 +852,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)
@@ -930,11 +929,6 @@ gtk_clapper_gl_widget_class_init (GtkClapperGLWidgetClass * klass)
"The pixel aspect ratio of the device", DEFAULT_PAR_N, DEFAULT_PAR_D,
G_MAXINT, 1, 1, 1, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (gobject_klass, PROP_IGNORE_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;
@@ -951,7 +945,9 @@ 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->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);
@@ -996,9 +992,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,4,0)
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)
@@ -1018,6 +1025,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,6 @@ struct _GtkClapperGLWidget
/* properties */
gboolean force_aspect_ratio;
gint par_n, par_d;
gboolean ignore_textures;
gint display_width;
gint display_height;
@@ -61,7 +60,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,9 @@ gstclapper_sources = [
'gstclapper-g-main-context-signal-dispatcher.c',
'gstclapper-video-overlay-video-renderer.c',
'gstclapper-visualization.c',
'gstclapper-playlist.c',
'gstclapper-playlist-item.c',
'gstclapper-mpris.c',
'gstclapper-gtk4-plugin.c',
'gtk4/gstclapperglsink.c',
@@ -23,6 +28,9 @@ gstclapper_headers = [
'gstclapper-g-main-context-signal-dispatcher.h',
'gstclapper-video-overlay-video-renderer.h',
'gstclapper-visualization.h',
'gstclapper-playlist.h',
'gstclapper-playlist-item.h',
'gstclapper-mpris.h',
'gstclapper-gtk4-plugin.h',
]
gstclapper_defines = [
@@ -40,10 +48,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 +62,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.0',
version: '0.2.1',
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.0
Version: 0.2.1
Maintainer: Rafostar <rafostar.github@gmail.com>
Build-Depends: debhelper (>= 10),
meson (>= 0.50),

View File

@@ -1,5 +1,5 @@
clapper (0.2.0) unstable; urgency=low
clapper (0.2.1) unstable; urgency=low
* New version
-- Rafostar <rafostar.github@gmail.com> Tue, 13 Apr 2021 09:39:00 +0100
-- Rafostar <rafostar.github@gmail.com> Mon, 19 Apr 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.0
Version: 0.2.1
Release: 1%{?dist}
Summary: Simple and modern GNOME media player
@@ -126,6 +126,9 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/*.desktop
%{_libdir}/%{appname}/
%changelog
* Mon Apr 19 2021 Rafostar <rafostar.github@gmail.com> - 0.2.1-1
- New version
* Tue Apr 13 2021 Rafostar <rafostar.github@gmail.com> - 0.2.0-1
- New version

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

60
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
{
@@ -18,8 +20,9 @@ class ClapperControls extends Gtk.Box
can_focus: false,
});
this.minFullViewWidth = 560;
this.currentPosition = 0;
this.currentDuration = 0;
this.isPositionDragging = false;
this.isMobile = false;
this.isFullscreen = false;
@@ -83,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));
}
@@ -101,8 +99,6 @@ class ClapperControls extends Gtk.Box
button.setFullscreenMode(isFullscreen);
this.unfullscreenButton.visible = isFullscreen;
this.can_focus = isFullscreen;
this.isFullscreen = isFullscreen;
}
@@ -114,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;
@@ -236,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);
@@ -248,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)
@@ -296,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);
@@ -473,7 +478,7 @@ class ClapperControls extends Gtk.Box
_onPlayerResize(width, height)
{
const isMobile = (width < 560);
const isMobile = (width < this.minFullViewWidth);
if(this.isMobile === isMobile)
return;
@@ -619,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();
}
@@ -228,6 +284,10 @@ class ClapperPrefsDialog extends Gtk.Dialog
{
title: 'Network',
widget: Prefs.NetworkPage,
},
{
title: 'YouTube',
widget: Prefs.YouTubePage,
}
]
},
@@ -253,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();
}
@@ -315,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');
@@ -151,7 +154,7 @@ class ClapperHeaderBarBase extends Gtk.Box
for(let name of layoutArr) {
/* Menu might be named "appmenu" */
if(!menuAdded && (!name || name === 'appmenu'))
if(!menuAdded && (!name || name === 'appmenu' || name === 'icon'))
name = 'menu';
const widget = this[`${name}Widget`];
@@ -196,6 +199,7 @@ class ClapperHeaderBarBase extends Gtk.Box
const button = new Gtk.Button({
icon_name: `window-${name}-symbolic`,
valign: Gtk.Align.CENTER,
can_focus: false,
});
button.add_css_class('circular');
@@ -263,7 +267,5 @@ class ClapperHeaderBarPopover extends Gtk.PopoverMenu
child.revealControls();
child.isPopoverOpen = false;
child.player.widget.grab_focus();
}
});

View File

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

@@ -3,34 +3,58 @@ const ByteArray = imports.byteArray;
const Debug = imports.src.debug;
const Misc = imports.src.misc;
const YouTube = imports.src.youtube;
const { PlayerBase } = imports.src.playerBase;
const { PlaylistWidget } = imports.src.playlist;
const { WebApp } = imports.src.webApp;
const { debug } = Debug;
const { debug, warn } = Debug;
const { settings } = Misc;
let WebServer;
var Player = GObject.registerClass(
class ClapperPlayer extends PlayerBase
class ClapperPlayer extends GstClapper.Clapper
{
_init()
{
super._init();
const gtk4plugin = new GstClapper.ClapperGtk4Plugin();
const glsinkbin = Gst.ElementFactory.make('glsinkbin', null);
glsinkbin.sink = gtk4plugin.video_sink;
super._init({
signal_dispatcher: new GstClapper.ClapperGMainContextSignalDispatcher(),
video_renderer: new GstClapper.ClapperVideoOverlayVideoRenderer({
video_sink: glsinkbin,
}),
mpris: new GstClapper.ClapperMpris({
own_name: `org.mpris.MediaPlayer2.${Misc.appName}`,
id_path: '/' + Misc.appId.replace(/\./g, '/'),
identity: Misc.appName,
desktop_entry: Misc.appId,
default_art_url: Misc.getClapperThemeIconUri(),
}),
});
this.widget = gtk4plugin.video_sink.widget;
this.widget.add_css_class('videowidget');
this.visualization_enabled = false;
this.webserver = null;
this.webapp = null;
this.ytClient = null;
this.playlistWidget = new PlaylistWidget();
this.seek_done = true;
this.needsFastSeekRestore = false;
this.customVideoTitle = null;
this.canAutoFullscreen = false;
this.playOnFullscreen = false;
this.windowMapped = 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();
this.connect('state-changed', this._onStateChanged.bind(this));
this.connect('uri-loaded', this._onUriLoaded.bind(this));
@@ -38,14 +62,82 @@ class ClapperPlayer extends PlayerBase
this.connect('warning', this._onPlayerWarning.bind(this));
this.connect('error', this._onPlayerError.bind(this));
settings.connect('changed', this._onSettingsKeyChanged.bind(this));
this._realizeSignal = this.widget.connect('realize', this._onWidgetRealize.bind(this));
}
set_and_bind_settings()
{
const settingsToSet = [
'seeking-mode',
'audio-offset',
'subtitle-offset',
'play-flags',
'webserver-enabled'
];
for(let key of settingsToSet)
this._onSettingsKeyChanged(settings, key);
const flag = Gio.SettingsBindFlags.GET;
settings.bind('subtitle-font', this.pipeline, 'subtitle_font_desc', flag);
}
set_initial_config()
{
this.set_mute(false);
/* FIXME: change into option in preferences */
const pipeline = this.get_pipeline();
pipeline.ring_buffer_max_size = 8 * 1024 * 1024;
}
set_all_plugins_ranks()
{
let data = [];
/* Set empty plugin list if someone messed it externally */
try {
data = JSON.parse(settings.get_string('plugin-ranking'));
if(!Array.isArray(data))
throw new Error('plugin ranking data is not an array!');
}
catch(err) {
debug(err);
settings.set_string('plugin-ranking', "[]");
}
for(let plugin of data) {
if(!plugin.apply || !plugin.name)
continue;
this.set_plugin_rank(plugin.name, plugin.rank);
}
}
set_plugin_rank(name, rank)
{
const gstRegistry = Gst.Registry.get();
const feature = gstRegistry.lookup_feature(name);
if(!feature) {
warn(`cannot change rank of unavailable plugin: ${name}`);
return;
}
const oldRank = feature.get_rank();
if(rank === oldRank)
return;
feature.set_rank(rank);
debug(`changed rank: ${oldRank} -> ${rank} for ${name}`);
}
set_uri(uri)
{
this.customVideoTitle = null;
if(Gst.Uri.get_protocol(uri) !== 'file') {
if(Misc.getUriProtocol(uri) !== 'file') {
const [isYouTubeUri, videoId] = YouTube.checkYouTubeUri(uri);
if(!isYouTubeUri)
@@ -54,7 +146,11 @@ class ClapperPlayer extends PlayerBase
if(!this.ytClient)
this.ytClient = new YouTube.YouTubeClient();
this.ytClient.getPlaybackDataAsync(videoId)
const { root } = this.widget;
const surface = root.get_surface();
const monitor = root.display.get_monitor_at_surface(surface);
this.ytClient.getPlaybackDataAsync(videoId, monitor)
.then(data => {
this.customVideoTitle = data.title;
super.set_uri(data.uri);
@@ -113,18 +209,41 @@ class ClapperPlayer extends PlayerBase
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();
}
const firstTrack = this.playlistWidget.get_row_at_index(0);
if(!firstTrack) return;
/* If not mapped yet, first track will play after map */
if(this.windowMapped)
this._playFirstTrack();
}
firstTrack.activate();
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)
@@ -133,7 +252,7 @@ class ClapperPlayer extends PlayerBase
/* Check local file existence */
if(
Gst.Uri.get_protocol(uri) === 'file'
Misc.getUriProtocol(uri) === 'file'
&& !Misc.getFileFromLocalUri(uri)
)
return;
@@ -179,7 +298,7 @@ class ClapperPlayer extends PlayerBase
seek_seconds(seconds)
{
this.seek(seconds * 1000000000);
this.seek(seconds * Gst.SECOND);
}
seek_chapter(seconds)
@@ -189,12 +308,9 @@ class ClapperPlayer extends PlayerBase
return;
}
/* FIXME: Remove this check when GstPlay(er) have set_seek_mode function */
if(this.set_seek_mode) {
this.set_seek_mode(GstClapper.ClapperSeekMode.DEFAULT);
this.seekingMode = 'normal';
this.needsFastSeekRestore = true;
}
this.set_seek_mode(GstClapper.ClapperSeekMode.DEFAULT);
this.seekingMode = 'normal';
this.needsFastSeekRestore = true;
this.seek_seconds(seconds);
}
@@ -242,13 +358,22 @@ class ClapperPlayer extends PlayerBase
controls.volumeScale.set_value(volume);
}
toggle_play()
next_chapter()
{
const action = (this.state === GstClapper.ClapperState.PLAYING)
? 'pause'
: 'play';
return this._switchChapter(false);
}
this[action]();
prev_chapter()
{
return this._switchChapter(true);
}
emitWs(action, value)
{
if(!this.webserver)
return;
this.webserver.sendMessage({ action, value });
}
receiveWs(action, value)
@@ -257,10 +382,26 @@ class ClapperPlayer extends PlayerBase
case 'toggle_play':
case 'play':
case 'pause':
this[action]();
break;
case 'seek':
case 'set_playlist':
case 'append_playlist':
case 'set_subtitles':
this[action](value);
break;
case 'change_playlist_item':
this.playlistWidget.changeActiveRow(value);
break;
case 'toggle_fullscreen':
case 'volume_up':
case 'volume_down':
case 'next_track':
case 'prev_track':
case 'next_chapter':
case 'prev_chapter':
this.widget.activate_action(`app.${action}`, null);
break;
case 'toggle_maximized':
action = 'toggle-maximized';
case 'minimize':
@@ -268,20 +409,47 @@ class ClapperPlayer extends PlayerBase
this.widget.activate_action(`window.${action}`, null);
break;
default:
const clapperWidget = this.widget.get_ancestor(Gtk.Grid);
switch(action) {
case 'toggle_fullscreen':
clapperWidget.toggleFullscreen();
break;
default:
super.receiveWs(action, value);
break;
}
warn(`unhandled WebSocket action: ${action}`);
break;
}
}
_switchChapter(isPrevious)
{
if(this.state === GstClapper.ClapperState.STOPPED)
return false;
const { chapters } = this.widget.root.child.controls;
if(!chapters)
return false;
const now = this.position / Gst.SECOND;
const chapterTimes = Object.keys(chapters).sort((a, b) => a - b);
if(isPrevious)
chapterTimes.reverse();
const chapter = chapterTimes.find(time => (isPrevious)
? now - 2.5 > time
: now < time
);
if(!chapter)
return false;
this.seek_chapter(chapter);
return true;
}
_addPlaylistItems(playlist)
{
for(let source of playlist) {
const uri = this._getSourceUri(source);
debug(`added uri: ${uri}`);
this.playlistWidget.addItem(uri);
}
}
_getSourceUri(source)
{
return (source.get_uri != null)
@@ -291,6 +459,14 @@ class ClapperPlayer extends PlayerBase
: Gst.filename_to_uri(source);
}
_playFirstTrack()
{
const firstTrack = this.playlistWidget.get_row_at_index(0);
if(!firstTrack) return;
firstTrack.activate();
}
_performCloseCleanup(window)
{
window.disconnect(this.closeRequestSignal);
@@ -312,8 +488,8 @@ class ClapperPlayer extends PlayerBase
let resumeInfo = {};
if(playlistItem.isLocalFile && settings.get_boolean('resume-enabled')) {
const resumeTime = Math.floor(this.position / 1000000000);
const resumeDuration = this.duration / 1000000000;
const resumeTime = Math.floor(this.position / Gst.SECOND);
const resumeDuration = this.duration / Gst.SECOND;
/* Do not save resume info when video is very short,
* just started or almost finished */
@@ -343,7 +519,6 @@ class ClapperPlayer extends PlayerBase
_onStateChanged(player, state)
{
this.state = state;
this.emitWs('state_changed', state);
if(state !== GstClapper.ClapperState.BUFFERING) {
@@ -387,7 +562,7 @@ class ClapperPlayer extends PlayerBase
debug(`end of stream: ${lastTrackId}`);
this.emitWs('end_of_stream', lastTrackId);
if(this.playlistWidget.nextTrack())
if(this.playlistWidget._handleStreamEnded(player))
return;
if(settings.get_boolean('close-auto')) {
@@ -395,6 +570,10 @@ class ClapperPlayer extends PlayerBase
this.quitOnStop = true;
this._performCloseCleanup(this.widget.get_root());
}
/* When this signal is connected player
* wants us to decide if it should stop */
this.stop();
}
_onUriLoaded(player, uri)
@@ -402,26 +581,7 @@ class ClapperPlayer extends PlayerBase
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)
@@ -457,69 +617,10 @@ class ClapperPlayer extends PlayerBase
);
}
/* Widget only - does not happen when using controls navigation */
_onWidgetKeyPressed(controller, keyval, keycode, state)
_onWindowMap(window)
{
const clapperWidget = this.widget.get_ancestor(Gtk.Grid);
let bool = false;
this.keyPressCount++;
switch(keyval) {
case Gdk.KEY_Up:
bool = true;
case Gdk.KEY_Down:
this.adjust_volume(bool);
break;
case Gdk.KEY_Right:
bool = true;
case Gdk.KEY_Left:
this.adjust_position(bool);
if(this.keyPressCount > 1)
clapperWidget.revealControls();
break;
default:
break;
}
}
/* Also happens after using controls navigation for selected keys */
_onWidgetKeyReleased(controller, keyval, keycode, state)
{
const clapperWidget = this.widget.get_ancestor(Gtk.Grid);
let value, root;
this.keyPressCount = 0;
switch(keyval) {
case Gdk.KEY_space:
this.toggle_play();
break;
case Gdk.KEY_Return:
if(clapperWidget.isFullscreenMode)
clapperWidget.revealControls(true);
break;
case Gdk.KEY_Right:
case Gdk.KEY_Left:
value = Math.round(
clapperWidget.controls.positionScale.get_value()
);
this.seek_seconds(value);
clapperWidget._setHideControlsTimeout();
break;
case Gdk.KEY_F11:
case Gdk.KEY_f:
case Gdk.KEY_F:
clapperWidget.toggleFullscreen();
break;
case Gdk.KEY_q:
case Gdk.KEY_Q:
root = this.widget.get_root();
root.emit('close-request');
break;
default:
break;
}
this.windowMapped = true;
this._playFirstTrack();
}
_onCloseRequest(window)
@@ -532,4 +633,109 @@ class ClapperPlayer extends PlayerBase
this.quitOnStop = true;
this.stop();
}
_onSettingsKeyChanged(settings, key)
{
let root, value, action;
switch(key) {
case 'seeking-mode':
this.seekingMode = settings.get_string('seeking-mode');
switch(this.seekingMode) {
case 'fast':
this.set_seek_mode(GstClapper.ClapperSeekMode.FAST);
break;
case 'accurate':
this.set_seek_mode(GstClapper.ClapperSeekMode.ACCURATE);
break;
default:
this.set_seek_mode(GstClapper.ClapperSeekMode.DEFAULT);
break;
}
break;
case 'render-shadows':
root = this.widget.get_root();
if(!root) break;
const gpuClass = 'gpufriendly';
const renderShadows = settings.get_boolean(key);
const hasShadows = !root.has_css_class(gpuClass);
if(renderShadows === hasShadows)
break;
action = (renderShadows) ? 'remove' : 'add';
root[action + '_css_class'](gpuClass);
break;
case 'audio-offset':
value = Math.round(settings.get_double(key) * -Gst.MSECOND);
this.set_audio_video_offset(value);
debug(`set audio-video offset: ${value}`);
break;
case 'subtitle-offset':
value = Math.round(settings.get_double(key) * -Gst.MSECOND);
this.set_subtitle_video_offset(value);
debug(`set subtitle-video offset: ${value}`);
break;
case 'dark-theme':
root = this.widget.get_root();
if(!root) break;
root.application._onThemeChanged(Gtk.Settings.get_default());
break;
case 'play-flags':
const initialFlags = this.pipeline.flags;
const settingsFlags = settings.get_int(key);
if(initialFlags === settingsFlags)
break;
this.pipeline.flags = settingsFlags;
debug(`changed play flags: ${initialFlags} -> ${settingsFlags}`);
break;
case 'webserver-enabled':
case 'webapp-enabled':
const webserverEnabled = settings.get_boolean('webserver-enabled');
if(webserverEnabled) {
if(!WebServer) {
/* Probably most users will not use this,
* so conditional import for faster startup */
WebServer = imports.src.webServer.WebServer;
}
if(!this.webserver) {
this.webserver = new WebServer(settings.get_int('webserver-port'));
this.webserver.passMsgData = this.receiveWs.bind(this);
}
this.webserver.startListening();
const webappEnabled = settings.get_boolean('webapp-enabled');
if(!this.webapp && !webappEnabled)
break;
if(webappEnabled) {
if(!this.webapp)
this.webapp = new WebApp();
this.webapp.startDaemonApp(settings.get_int('webapp-port'));
}
}
else if(this.webserver) {
/* remote app will close when connection is lost
* which will cause the daemon to close too */
this.webserver.stopListening();
}
break;
case 'webserver-port':
if(!this.webserver)
break;
this.webserver.setListeningPort(settings.get_int(key));
break;
default:
break;
}
}
});

View File

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

View File

@@ -1,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,13 +246,14 @@ 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]
);
this.isLocalFile = true;
}
this.filename = filename || uri;
this.set_tooltip_text(this.filename);
const box = new Gtk.Box({
orientation: Gtk.Orientation.HORIZONTAL,
@@ -127,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,
@@ -138,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
@@ -171,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

@@ -132,6 +132,22 @@ class ClapperNetworkPage extends PrefsBase.Grid
}
});
var YouTubePage = GObject.registerClass(
class ClapperYouTubePage extends PrefsBase.Grid
{
_init()
{
super._init();
this.addTitle('YouTube');
this.addCheckButton('Prefer adaptive streaming', 'yt-adaptive-enabled');
this.addComboBoxText('Max quality', [
['normal', "Normal"],
['hfr', "HFR"],
], 'yt-quality-type');
}
});
var GStreamerPage = GObject.registerClass(
class ClapperGStreamerPage extends PrefsBase.Grid
{

View File

@@ -370,6 +370,18 @@ class ClapperButtonsRevealer extends Gtk.Revealer
this.get_child().append(widget);
}
revealInstantly(isReveal)
{
if(this.child_revealed === isReveal)
return;
const initialDuration = this.transition_duration;
this.transition_duration = 0;
this.reveal_child = isReveal;
this.transition_duration = initialDuration;
}
_setRotateClass(icon, isAdd)
{
const cssClass = 'halfrotate';
@@ -388,7 +400,8 @@ class ClapperButtonsRevealer extends Gtk.Revealer
_onRevealChild(button)
{
this._setRotateClass(button.child, true);
if(this.reveal_child !== this.child_revealed)
this._setRotateClass(button.child, true);
}
_onChildRevealed(button)

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;
@@ -371,7 +353,7 @@ class ClapperWidget extends Gtk.Grid
return;
}
const pos = Math.floor(start / 1000000) / 1000;
const pos = Math.floor(start / Gst.MSECOND) / 1000;
const tags = subentry.get_tags();
this.controls.positionScale.add_mark(pos, Gtk.PositionType.TOP, null);
@@ -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');
@@ -467,17 +447,12 @@ class ClapperWidget extends Gtk.Grid
_onPlayerDurationChanged(player, duration)
{
const durationSeconds = duration / 1000000000;
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();
@@ -515,7 +490,7 @@ class ClapperWidget extends Gtk.Grid
)
return;
const positionSeconds = Math.round(position / 1000000000);
const positionSeconds = Math.round(position / Gst.SECOND);
if(positionSeconds === this.controls.currentPosition)
return;
@@ -541,6 +516,12 @@ class ClapperWidget extends Gtk.Grid
if(width === this.layoutWidth)
return;
/* Launch without showing revealers transitions on mobile width */
if(!this.layoutWidth && width < this.controls.minFullViewWidth) {
for(let revealer of this.controls.revealersArr)
revealer.revealInstantly(false);
}
this.layoutWidth = width;
if(this.isFullscreenMode)
@@ -552,26 +533,68 @@ 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)
@@ -611,7 +634,6 @@ class ClapperWidget extends Gtk.Grid
this.revealerTop.revealChild(false);
this.revealerBottom.revealChild(false);
}
this.setControlsCanFocus(false);
return GLib.SOURCE_REMOVE;
});
@@ -624,6 +646,7 @@ class ClapperWidget extends Gtk.Grid
&& !this.isMobileMonitor
&& !this._updateTimeTimeout
) {
debug('setting update time interval');
this._setUpdateTimeInterval();
}
}
@@ -698,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));
@@ -758,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)
@@ -907,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 => {
@@ -919,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

@@ -3,6 +3,7 @@ const Dash = imports.src.dash;
const Debug = imports.src.debug;
const FileOps = imports.src.fileOps;
const Misc = imports.src.misc;
const YTItags = imports.src.youtubeItags;
const YTDL = imports.src.assets['node-ytdl-core'];
const debug = Debug.ytDebug;
@@ -304,7 +305,7 @@ var YouTubeClient = GObject.registerClass({
});
}
async getPlaybackDataAsync(videoId)
async getPlaybackDataAsync(videoId, monitor)
{
const info = await this.getVideoInfoPromise(videoId).catch(debug);
@@ -312,28 +313,40 @@ var YouTubeClient = GObject.registerClass({
throw new Error('no YouTube video info');
let uri = null;
const dashInfo = await this.getDashInfoAsync(info).catch(debug);
const itagOpts = {
width: monitor.geometry.width * monitor.scale_factor,
height: monitor.geometry.height * monitor.scale_factor,
codec: 'h264',
type: settings.get_string('yt-quality-type'),
adaptive: settings.get_boolean('yt-adaptive-enabled'),
};
if(dashInfo) {
debug('parsed video info to dash info');
const dash = Dash.generateDash(dashInfo);
uri = await this.getHLSUriAsync(info, itagOpts);
if(dash) {
debug('got dash data');
if(!uri) {
const dashInfo = await this.getDashInfoAsync(info, itagOpts).catch(debug);
const dashFile = await FileOps.saveFilePromise(
'tmp', null, 'clapper.mpd', dash
).catch(debug);
if(dashInfo) {
debug('parsed video info to dash info');
const dash = Dash.generateDash(dashInfo);
if(dashFile)
uri = dashFile.get_uri();
if(dash) {
debug('got dash data');
debug('got dash file');
const dashFile = await FileOps.saveFilePromise(
'tmp', null, 'clapper.mpd', dash
).catch(debug);
if(dashFile)
uri = dashFile.get_uri();
debug('got dash file');
}
}
}
if(!uri)
uri = this.getBestCombinedUri(info);
uri = this.getBestCombinedUri(info, itagOpts);
if(!uri)
throw new Error('no YouTube video URI');
@@ -349,7 +362,61 @@ var YouTubeClient = GObject.registerClass({
return { uri, title };
}
async getDashInfoAsync(info)
async getHLSUriAsync(info, itagOpts)
{
const isLive = (
info.videoDetails.isLiveContent
&& (!info.videoDetails.lengthSeconds
|| Number(info.videoDetails.lengthSeconds) <= 0)
);
debug(`video is live: ${isLive}`);
/* YouTube only uses HLS for live content */
if(!isLive)
return null;
const hlsUri = info.streamingData.hlsManifestUrl;
if(!hlsUri) {
/* HLS may be unavailable on finished live streams */
debug('no HLS manifest URL');
return null;
}
if(!itagOpts.adaptive) {
const result = await this._downloadDataPromise(hlsUri).catch(debug);
if(!result || !result.data) {
debug(new Error('HLS manifest download failed'));
return hlsUri;
}
const hlsArr = result.data.split('\n');
const hlsStreams = [];
let index = hlsArr.length;
while(index--) {
const url = hlsArr[index];
if(!Gst.Uri.is_valid(url))
continue;
const itagIndex = url.indexOf('/itag/') + 6;
const itag = url.substring(itagIndex, itagIndex + 2);
hlsStreams.push({ itag, url });
}
debug(`obtaining HLS itags for resolution: ${itagOpts.width}x${itagOpts.height}`);
const hlsItags = YTItags.getHLSItags(itagOpts);
debug(`HLS itags: ${JSON.stringify(hlsItags)}`);
const hlsStream = this.getBestStreamFromItags(hlsStreams, hlsItags);
if(hlsStream)
return hlsStream.url;
}
return hlsUri;
}
async getDashInfoAsync(info, itagOpts)
{
if(
!info.streamingData
@@ -358,22 +425,9 @@ var YouTubeClient = GObject.registerClass({
)
return null;
/* TODO: Options in prefs to set preferred video formats and adaptive streaming */
const isAdaptiveEnabled = settings.get_boolean('yt-adaptive-enabled');
const allowedFormats = {
video: [
133,
134,
135,
136,
137,
298,
299,
],
audio: [
140,
]
};
debug(`obtaining DASH itags for resolution: ${itagOpts.width}x${itagOpts.height}`);
const dashItags = YTItags.getDashItags(itagOpts);
debug(`DASH itags: ${JSON.stringify(dashItags)}`);
const filteredStreams = {
video: [],
@@ -382,11 +436,11 @@ var YouTubeClient = GObject.registerClass({
for(let fmt of ['video', 'audio']) {
debug(`filtering ${fmt} streams`);
let index = allowedFormats[fmt].length;
let index = dashItags[fmt].length;
while(index--) {
const itag = allowedFormats[fmt][index];
const foundStream = info.streamingData.adaptiveFormats.find(stream => (stream.itag == itag));
const itag = dashItags[fmt][index];
const foundStream = info.streamingData.adaptiveFormats.find(stream => stream.itag == itag);
if(foundStream) {
/* Parse and convert mimeType string into object */
foundStream.mimeInfo = this._getMimeInfo(foundStream.mimeType);
@@ -401,7 +455,7 @@ var YouTubeClient = GObject.registerClass({
filteredStreams[fmt].unshift(foundStream);
debug(`added ${fmt} itag: ${foundStream.itag}`);
if(!isAdaptiveEnabled)
if(!itagOpts.adaptive)
break;
}
}
@@ -417,8 +471,16 @@ var YouTubeClient = GObject.registerClass({
for(let stream of fmtArr) {
debug(`initial URL: ${stream.url}`);
const result = await this._downloadDataPromise(stream.url, 'HEAD').catch(debug);
if(!result) return null;
/* Errors in some cases are to be expected here,
* so be quiet about them and use fallback methods */
const result = await this._downloadDataPromise(
stream.url, 'HEAD'
).catch(err => debug(err.message));
if(!result || !result.uri) {
debug('redirect could not be resolved');
return null;
}
stream.url = Misc.encodeHTML(result.uri)
.replace('?', '/')
@@ -440,16 +502,21 @@ var YouTubeClient = GObject.registerClass({
};
}
getBestCombinedUri(info)
getBestCombinedUri(info, itagOpts)
{
debug('obtaining best combined URL');
debug(`obtaining best combined URL for resolution: ${itagOpts.width}x${itagOpts.height}`);
const streams = info.streamingData.formats;
if(!info.streamingData.formats.length)
if(!streams.length)
return null;
const combinedStream = info.streamingData.formats[
info.streamingData.formats.length - 1
];
const combinedItags = YTItags.getCombinedItags(itagOpts);
let combinedStream = this.getBestStreamFromItags(streams, combinedItags);
if(!combinedStream) {
debug('trying any combined stream as last resort');
combinedStream = streams[streams.length - 1];
}
if(!combinedStream || !combinedStream.url)
return null;
@@ -457,6 +524,23 @@ var YouTubeClient = GObject.registerClass({
return combinedStream.url;
}
getBestStreamFromItags(streams, itags)
{
let index = itags.length;
while(index--) {
const itag = itags[index];
const stream = streams.find(stream => stream.itag == itag);
if(stream) {
debug(`found preferred stream itag: ${stream.itag}`);
return stream;
}
}
debug('could not find preferred stream for itags');
return null;
}
compareLastVideoId(videoId)
{
if(!this.lastInfo)
@@ -506,7 +590,7 @@ var YouTubeClient = GObject.registerClass({
return resolve(result);
}
debug(new Error(`response code: ${statusCode}`));
debug(`response code: ${statusCode}`);
/* Internal Soup codes mean download aborted
* or some other error that cannot be handled
@@ -645,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}`,
@@ -694,14 +779,21 @@ var YouTubeClient = GObject.registerClass({
_getIsCipher(data)
{
/* Check only first best combined,
* AFAIK there are no videos without it */
if(data.formats[0].url)
const stream = (data.formats.length)
? data.formats[0]
: data.adaptiveFormats[0];
if(!stream) {
debug(new Error('no streams'));
return false;
}
if(stream.url)
return false;
if(
data.formats[0].signatureCipher
|| data.formats[0].cipher
stream.signatureCipher
|| stream.cipher
)
return true;

80
src/youtubeItags.js Normal file
View File

@@ -0,0 +1,80 @@
const Itags = {
video: {
h264: {
normal: {
240: 133,
360: 134,
480: 135,
720: 136,
1080: 137,
},
hfr: {
720: 298,
1080: 299,
},
},
},
audio: {
aac: [140],
opus: [249, 250, 251],
},
combined: {
360: 18,
720: 22,
},
hls: {
240: 92,
360: 93,
480: 94,
720: 95,
1080: 96,
}
};
function _appendItagArray(arr, opts, formats)
{
const keys = Object.keys(formats);
for(let fmt of keys) {
arr.push(formats[fmt]);
if(
fmt >= opts.height
|| Math.floor(fmt * 16 / 9) >= opts.width
)
break;
}
return arr;
}
function getDashItags(opts)
{
const allowed = {
video: [],
audio: (opts.codec === 'h264')
? Itags.audio.aac
: Itags.audio.opus
};
const types = Object.keys(Itags.video[opts.codec]);
for(let type of types) {
const formats = Itags.video[opts.codec][type];
_appendItagArray(allowed.video, opts, formats);
if(type === opts.type)
break;
}
return allowed;
}
function getCombinedItags(opts)
{
return _appendItagArray([], opts, Itags.combined);
}
function getHLSItags(opts)
{
return _appendItagArray([], opts, Itags.hls);
}

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>