├── .gitignore
├── .vscode
└── tasks.json
├── LICENSE
├── README.md
├── chrome-ext
├── icon.png
├── icon_128.png
├── manifest.json
└── run.js
├── dist
├── config
│ └── config.xml
└── packages
│ ├── libvlc
│ └── meta
│ │ └── package.xml
│ ├── twitch_player
│ └── meta
│ │ └── package.xml
│ └── twitchd
│ └── meta
│ └── package.xml
├── player
├── .gitignore
├── .vscode
│ ├── launch.json
│ └── tasks.json
├── forms
│ ├── about_dialog.ui
│ ├── filters_tool.ui
│ ├── main_window.ui
│ ├── options_dialog.ui
│ ├── stream_card.ui
│ ├── stream_details.ui
│ ├── stream_picker.ui
│ ├── video_controls.ui
│ └── vlc_log_viewer.ui
├── include
│ ├── api
│ │ ├── oauth.hpp
│ │ ├── pubsub.hpp
│ │ ├── twitch.hpp
│ │ └── twitchd.hpp
│ ├── constants.hpp
│ ├── libvlc
│ │ ├── bindings.hpp
│ │ ├── event_watcher.hpp
│ │ ├── logger.hpp
│ │ └── types.hpp
│ ├── prelude
│ │ ├── c_wrapper.hpp
│ │ ├── http.hpp
│ │ ├── promise.hpp
│ │ ├── sync.hpp
│ │ ├── timer.hpp
│ │ └── variant.hpp
│ ├── process
│ │ └── daemon_control.hpp
│ └── ui
│ │ ├── layouts
│ │ ├── flow.hpp
│ │ └── splitter_grid.hpp
│ │ ├── main_window.hpp
│ │ ├── native
│ │ ├── capabilities.hpp
│ │ ├── osx.hpp
│ │ ├── win32.hpp
│ │ └── x11.hpp
│ │ ├── overlays
│ │ ├── video_controls.hpp
│ │ └── video_details.hpp
│ │ ├── tools
│ │ ├── about_dialog.hpp
│ │ ├── options_dialog.hpp
│ │ ├── video_filters.hpp
│ │ └── vlc_log_viewer.hpp
│ │ ├── tray.hpp
│ │ ├── utils
│ │ └── event_notifier.hpp
│ │ └── widgets
│ │ ├── chat_pane.hpp
│ │ ├── foreign_widget.hpp
│ │ ├── stream_card.hpp
│ │ ├── stream_pane.hpp
│ │ ├── stream_picker.hpp
│ │ ├── stream_widget.hpp
│ │ └── video_widget.hpp
├── player.pro
├── resources
│ ├── browse.png
│ ├── clock.svg
│ ├── dashboard.png
│ ├── fast_forward.png
│ ├── fullscreen_enter.png
│ ├── fullscreen_exit.png
│ ├── icon.ico
│ ├── joystick.svg
│ ├── kappa.png
│ ├── layout_left.png
│ ├── layout_right.png
│ ├── player.qrc
│ ├── remove.png
│ ├── unzoom.png
│ ├── viewers.svg
│ ├── volume_off.png
│ ├── volume_on.png
│ └── zoom.png
├── scripts
│ └── win32
│ │ ├── build.bat
│ │ └── qmake.bat
├── src
│ ├── api
│ │ ├── oauth.cpp
│ │ ├── pubsub.cpp
│ │ ├── twitch.cpp
│ │ └── twitchd.cpp
│ ├── libvlc
│ │ ├── bindings.cpp
│ │ ├── event_watcher.cpp
│ │ └── logger.cpp
│ ├── main.cpp
│ ├── process
│ │ └── daemon_control.cpp
│ └── ui
│ │ ├── layouts
│ │ ├── flow.cpp
│ │ └── splitter_grid.cpp
│ │ ├── main_window.cpp
│ │ ├── main_window_shortcuts.cpp
│ │ ├── native
│ │ ├── osx.cpp
│ │ ├── win32.cpp
│ │ └── x11.cpp
│ │ ├── overlays
│ │ ├── video_controls.cpp
│ │ └── video_details.cpp
│ │ ├── tools
│ │ ├── about_dialog.cpp
│ │ ├── options_dialog.cpp
│ │ ├── video_filters.cpp
│ │ └── vlc_log_viewer.cpp
│ │ ├── tray.cpp
│ │ ├── utils
│ │ └── event_notifier.cpp
│ │ └── widgets
│ │ ├── chat_pane.cpp
│ │ ├── foreign_widget.cpp
│ │ ├── stream_card.cpp
│ │ ├── stream_pane.cpp
│ │ ├── stream_picker.cpp
│ │ ├── stream_widget.cpp
│ │ └── video_widget.cpp
└── vendor
│ └── qtpromise-0.3.0
│ ├── .gitignore
│ ├── .travis.yml
│ ├── LICENSE
│ ├── README.md
│ ├── book.json
│ ├── docs
│ ├── README.md
│ ├── SUMMARY.md
│ ├── assets
│ │ └── style.css
│ └── qtpromise
│ │ ├── api-reference.md
│ │ ├── getting-started.md
│ │ ├── helpers
│ │ ├── qpromise.md
│ │ └── qpromiseall.md
│ │ ├── qpromise
│ │ ├── all.md
│ │ ├── constructor.md
│ │ ├── delay.md
│ │ ├── fail.md
│ │ ├── finally.md
│ │ ├── isfulfilled.md
│ │ ├── ispending.md
│ │ ├── isrejected.md
│ │ ├── reject.md
│ │ ├── resolve.md
│ │ ├── tap.md
│ │ ├── then.md
│ │ ├── timeout.md
│ │ └── wait.md
│ │ ├── qtconcurrent.md
│ │ └── thread-safety.md
│ ├── include
│ └── QtPromise
│ ├── package
│ └── features
│ │ └── qtpromise.prf
│ ├── qpm.json
│ ├── qtpromise.pri
│ ├── qtpromise.pro
│ ├── src
│ ├── qtpromise
│ │ ├── qpromise.h
│ │ ├── qpromise.inl
│ │ ├── qpromise_p.h
│ │ ├── qpromiseerror.h
│ │ ├── qpromisefuture.h
│ │ ├── qpromiseglobal.h
│ │ ├── qpromisehelpers.h
│ │ ├── qtpromise.pri
│ │ └── qtpromise.pro
│ └── src.pro
│ └── tests
│ ├── auto
│ ├── auto.pro
│ └── qtpromise
│ │ ├── benchmark
│ │ ├── benchmark.pro
│ │ └── tst_benchmark.cpp
│ │ ├── future
│ │ ├── future.pro
│ │ └── tst_future.cpp
│ │ ├── helpers
│ │ ├── helpers.pro
│ │ └── tst_helpers.cpp
│ │ ├── qpromise
│ │ ├── all
│ │ │ ├── all.pro
│ │ │ └── tst_all.cpp
│ │ ├── construct
│ │ │ ├── construct.pro
│ │ │ └── tst_construct.cpp
│ │ ├── delay
│ │ │ ├── delay.pro
│ │ │ └── tst_delay.cpp
│ │ ├── fail
│ │ │ ├── fail.pro
│ │ │ └── tst_fail.cpp
│ │ ├── finally
│ │ │ ├── finally.pro
│ │ │ └── tst_finally.cpp
│ │ ├── operators
│ │ │ ├── operators.pro
│ │ │ └── tst_operators.cpp
│ │ ├── qpromise.pro
│ │ ├── resolve
│ │ │ ├── resolve.pro
│ │ │ └── tst_resolve.cpp
│ │ ├── tap
│ │ │ ├── tap.pro
│ │ │ └── tst_tap.cpp
│ │ ├── then
│ │ │ ├── then.pro
│ │ │ └── tst_then.cpp
│ │ └── timeout
│ │ │ ├── timeout.pro
│ │ │ └── tst_timeout.cpp
│ │ ├── qtpromise.pri
│ │ ├── qtpromise.pro
│ │ ├── requirements
│ │ ├── requirements.pro
│ │ └── tst_requirements.cpp
│ │ ├── shared
│ │ └── utils.h
│ │ └── thread
│ │ ├── thread.pro
│ │ └── tst_thread.cpp
│ └── tests.pro
├── scripts
└── win32
│ └── bundle-dist.bat
└── twitchd
├── .gitignore
├── .vscode
├── launch.json
└── tasks.json
├── Cargo.lock
├── Cargo.toml
├── scripts
└── win32
│ └── release.bat
├── src
├── main.rs
├── options.rs
├── prelude
│ ├── http.rs
│ ├── mod.rs
│ ├── runtime.rs
│ └── stream_ext.rs
├── server
│ ├── mod.rs
│ ├── service.rs
│ └── state
│ │ ├── index_cache.rs
│ │ ├── mod.rs
│ │ ├── player_pool.rs
│ │ └── stream_player.rs
└── twitch
│ ├── api.rs
│ ├── m3u8.rs
│ ├── mod.rs
│ ├── mpeg_ts.rs
│ ├── types.rs
│ └── utils.rs
└── test_samples
├── m3u8
├── faulty_playlist_01_august_2019.m3u8
├── faulty_playlist_11_october_2019.m3u8
├── faulty_playlist_22_may_2019.m3u8
├── index_with_restricted_playlist.m3u8
├── index_without_program_id.m3u8
├── playlist_with_map.m3u8
├── playlist_with_prefetch_segments.m3u8
├── playlist_with_program_date_time.m3u8
├── playlist_with_stitched_ads.m3u8
├── simple_index.m3u8
└── simple_playlist.m3u8
└── mpeg_ts
├── encoded_160p.ts
├── encoded_480p.ts
├── encoded_720p60.ts
├── source_1080p60.ts
├── source_faulty_21_feb_2020.ts
└── source_faulty_22_may_2019.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/packages/**/data/*
2 | dist/*.exe
3 | dist/twitch-player_*
4 | .vscode/settings.json
5 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "label": "bundle dist",
8 | "type": "shell",
9 | "windows": {
10 | "command": "${workspaceFolder}/scripts/win32/bundle-dist.bat"
11 | },
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Guillaume Depardon
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # twitch-player
2 | performance focused twitch chat and video playback
3 |
4 | ## Motivation
5 | Video playback of [Twitch](https://twitch.tv) streams inside web browsers seems to use an unusally large mount of resources, especially cpu-wise.
6 | When you use native video players like [vlc](https://www.videolan.org/vlc/index.html) or [mpc](https://mpc-hc.org/), you can observe up to ~4 times less cpu usage.
7 |
8 | Projects like [livestreamer](https://github.com/chrippa/livestreamer/releases) or [streamlink](https://github.com/streamlink/streamlink-twitch-gui) already exist but they lack an essential feature: a playback layout that includes chat.
9 | This project aims at solving this specific issue by "simply" embedding a native player and a chat window inside a shared layout so that it looks like this:
10 | 
11 |
12 | You get basic resizing functionalities, with an adjustable splitter between the video player and the chat, some controls to enable fullscreen/toggle chat/...
13 | You also get multistream support and a bunch of cool playback features.
14 |
15 | ## Architecture
16 | To circumvent some playback seeking limitations of vlc for live hls playlists like Twitch's and to anticipate streaming format changes, this project also includes its own hls fetcher named `twitchd`.
17 | This component can live on its own and can be tweaked to fit your specs and internet connection perfectly. It can actually yield better "latency to broadcaster" than the twitch.tv player. It can also be used as a proxy and broadcast entrypoint.
18 |
19 | The graphical interface is written to target platforms natively. It relies on `libvlc` to display the video and on some instance of `twitchd` to get the video data.
20 | This project currently does not feature a custom chat implementation and therefore relies on the native platform to embed an external window - typically a web browser - that will render chat.
21 |
22 | ## Building
23 |
24 | ### twitchd
25 | this daemon aims at being lightweight and performant, and is therefore written in rust.
26 |
27 | With a stable [rust toolchain](https://www.rustup.rs/), you can just run:
28 | ```sh
29 | cargo build --release
30 | ```
31 | inside the `./twitchd` directory and that should be enough to build a native executable for this component.
32 |
33 | ### UI
34 |
35 | The UI aims at targetting all platforms. [Electron does not support embedding of foreign windows](https://github.com/electron/electron/issues/10547) in its current state and I couldn't find any other web based toolkit that fit my needs. I ended up picking Qt and the language of choice for that framework is C++
36 |
37 | With a recent version of [Qt](https://www.qt.io/download) (>= 5.10), a recent version of libVLC (> 3.0.0) and a C++17 compliant compiler you should be able to build the UI by running:
38 | ```sh
39 | export LIBVLC_INCLUDE_DIR="path/to/your/libvlc/include"
40 | export LIBVLC_LIB_DIR="path/to/your/libvlc/lib"
41 | mkdir target && cd target
42 | qmake ..
43 | make release # or nmake on Windows
44 | ```
45 | inside the `./player` directory.
46 | This has been tested on Windows with Visual Studio 2017 and also on linux with clang 6.0
47 |
--------------------------------------------------------------------------------
/chrome-ext/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Globidev/twitch-player/062306685e182c422980d57fe7ba9fbdee141c37/chrome-ext/icon.png
--------------------------------------------------------------------------------
/chrome-ext/icon_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Globidev/twitch-player/062306685e182c422980d57fe7ba9fbdee141c37/chrome-ext/icon_128.png
--------------------------------------------------------------------------------
/chrome-ext/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 |
4 | "name": "Open in Twitch Player",
5 | "description": "Runs Twitch web streams in Twitch Player",
6 | "version": "0.1",
7 |
8 | "permissions": [
9 | "activeTab"
10 | ],
11 | "browser_action": {
12 | "default_icon": "icon.png"
13 | },
14 | "background": {
15 | "scripts": ["run.js"],
16 | "persistent": false
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/chrome-ext/run.js:
--------------------------------------------------------------------------------
1 | chrome.browserAction.onClicked.addListener(function(tab) {
2 | const url = new URL(tab.url),
3 | channel = url.pathname.slice(1);
4 |
5 | if (url.host == 'www.twitch.tv' && !channel.includes('/')) {
6 | chrome.tabs.executeScript({
7 | code: `window.location="twitch-player://${channel}"`
8 | });
9 | setTimeout(() => chrome.tabs.remove(tab.id, () => {}), 3000)
10 | }
11 |
12 | });
13 |
--------------------------------------------------------------------------------
/dist/config/config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Twitch Player
4 | 0.1.0
5 | Twitch-Player Installer
6 | Globi
7 | Twitch Player
8 | @ApplicationsDir@/TwitchPlayer
9 |
10 |
11 | http://glo.bi/twitch-player/
12 | 1
13 | Globi repository
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/dist/packages/libvlc/meta/package.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | libvlc
4 | Install the libvlc dependencies (RECOMMENDED)
5 | 3.0.6
6 | 2019-02-02
7 | true
8 | Bump libvlc version
9 |
10 |
--------------------------------------------------------------------------------
/dist/packages/twitch_player/meta/package.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Twitch Player
4 | Install the end user interface
5 | 0.9.28
6 | 2019-10-11
7 | true
8 | true
9 | Bump daemon version
10 |
11 |
--------------------------------------------------------------------------------
/dist/packages/twitchd/meta/package.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Twitchd
4 | Install the Twitch API daemon
5 | 1.4.8
6 | 2019-10-11
7 | true
8 | true
9 | Handled new playlist format changes
10 |
11 |
--------------------------------------------------------------------------------
/player/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | .vscode/c_cpp_properties.json
3 | .vscode/ipch
4 |
--------------------------------------------------------------------------------
/player/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 |
8 | {
9 | "name": "(Windows) Launch",
10 | "type": "cppvsdbg",
11 | "request": "launch",
12 | "program": "${workspaceFolder}/target/debug/twitch-player.exe",
13 | "args": [
14 | // "giantwaffle"
15 | ],
16 | "stopAtEntry": false,
17 | "cwd": "${workspaceFolder}",
18 | "environment": [],
19 | "externalConsole": true,
20 | "preLaunchTask": "dev: compile (debug)"
21 | }
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/player/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "label": "dev: qmake",
8 | "group": "build",
9 | "type": "shell",
10 | "windows": {
11 | "command": "${workspaceFolder}/scripts/win32/qmake.bat"
12 | }
13 | },
14 | {
15 | "label": "dev: compile (debug)",
16 | "group": {
17 | "kind": "build",
18 | "isDefault": true
19 | },
20 | "type": "shell",
21 | "windows": {
22 | "command": "${workspaceFolder}/scripts/win32/build.bat",
23 | "args": ["debug"]
24 | },
25 | },
26 | {
27 | "label": "deploy: qmake",
28 | "group": "build",
29 | "type": "shell",
30 | "windows": {
31 | "command": "${workspaceFolder}/scripts/win32/qmake.bat",
32 | "args": ["deploy"]
33 | }
34 | },
35 | {
36 | "label": "deploy: compile (release)",
37 | "group": "build",
38 | "type": "shell",
39 | "windows": {
40 | "command": "${workspaceFolder}/scripts/win32/build.bat",
41 | "args": ["release"]
42 | },
43 | },
44 | ]
45 | }
46 |
--------------------------------------------------------------------------------
/player/forms/about_dialog.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | AboutDialog
4 |
5 |
6 |
7 | 0
8 | 0
9 | 239
10 | 42
11 |
12 |
13 |
14 | About Twitch Player
15 |
16 |
17 | -
18 |
19 |
20 | Qt::Horizontal
21 |
22 |
23 |
24 | 40
25 | 20
26 |
27 |
28 |
29 |
30 | -
31 |
32 |
33 |
34 | 16
35 | 16
36 |
37 |
38 |
39 |
40 |
41 |
42 | :/icons/icon.ico
43 |
44 |
45 | false
46 |
47 |
48 |
49 | -
50 |
51 |
52 | Twitch Player version
53 |
54 |
55 |
56 | -
57 |
58 |
59 | Qt::Horizontal
60 |
61 |
62 |
63 | 40
64 | 20
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/player/include/api/oauth.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 |
6 | class QTcpServer;
7 | class QNetworkRequest;
8 |
9 | using QtPromise::QPromise;
10 |
11 | class OAuth: public QObject {
12 | Q_OBJECT
13 |
14 | public:
15 | OAuth(QObject * = nullptr);
16 |
17 | QPromise query_token();
18 |
19 | signals:
20 | void token_ready(QString);
21 |
22 | private:
23 | QTcpServer *_local_server;
24 |
25 | void fetch_token(const QUrl &);
26 | void save_token_data(const QByteArray &);
27 | };
28 |
--------------------------------------------------------------------------------
/player/include/api/pubsub.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | #include
6 |
7 | class QWebSocket;
8 | class QTimer;
9 |
10 | struct PendingOrder {
11 | QtPromise::QPromiseResolve resolve;
12 | QtPromise::QPromiseReject reject;
13 | };
14 |
15 | class TwitchPubSub: public QObject {
16 | Q_OBJECT
17 |
18 | public:
19 | TwitchPubSub(QObject * = nullptr);
20 |
21 | QtPromise::QPromise listen_to_channel(QString);
22 | void unlisten_to_channel(QString);
23 |
24 | signals:
25 | void channel_went_live(QString);
26 | void channel_went_offline(QString);
27 |
28 | private:
29 | void send_message(QJsonObject);
30 | void process_message(QJsonObject);
31 |
32 | QWebSocket *_ws;
33 | QTimer *_ping_timer;
34 |
35 | QMap _pending_orders;
36 |
37 | QList _order_queue;
38 | QList _orders_issued;
39 | };
40 |
--------------------------------------------------------------------------------
/player/include/api/twitch.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "prelude/http.hpp"
4 |
5 | struct ChannelData {
6 | QString name, display_name;
7 | QString title;
8 | QString logo_url;
9 | uint32_t id;
10 | };
11 |
12 | struct StreamData {
13 | ChannelData channel;
14 | QString preview;
15 | QString current_game;
16 | uint32_t viewcount;
17 | QDateTime created_at;
18 | };
19 |
20 | struct TwitchAPI: APIClient {
21 | using streams_response_t = response_t>;
22 |
23 | streams_response_t stream_search(QString);
24 | streams_response_t top_streams();
25 | streams_response_t followed_streams(const QString &);
26 |
27 | using stream_response_t = response_t;
28 | stream_response_t stream(uint32_t);
29 |
30 | using channels_response_t = response_t>;
31 | channels_response_t channel_search(const QString &);
32 | };
33 |
--------------------------------------------------------------------------------
/player/include/api/twitchd.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "prelude/http.hpp"
4 |
5 | struct Resolution {
6 | uint32_t width, height;
7 | };
8 |
9 | struct StreamInfo {
10 | Resolution resolution;
11 | uint64_t bandwidth;
12 | };
13 |
14 | struct MediaInfo {
15 | QString name;
16 | QString group_id;
17 | };
18 |
19 | struct PlaylistInfo {
20 | StreamInfo stream_info;
21 | MediaInfo media_info;
22 | QString url;
23 | };
24 |
25 | struct StreamIndex {
26 | QList playlist_infos;
27 | };
28 |
29 | struct SegmentMetadata {
30 | quint32 broadc_s;
31 | QString cmd;
32 | quint32 ingest_r, ingest_s;
33 | double stream_offset;
34 | quint64 transc_r, transc_s;
35 | };
36 |
37 | struct TwitchdAPI: APIClient {
38 | using stream_index_response_t = response_t;
39 | stream_index_response_t stream_index(QString);
40 |
41 | using metadata_response_t = response_t;
42 | metadata_response_t metadata(QString, QString, QString);
43 |
44 | using daemon_version_response_t = response_t;
45 | daemon_version_response_t daemon_version();
46 |
47 | using daemon_quit_response_t = response_t;
48 | daemon_quit_response_t daemon_quit();
49 |
50 | static QString playback_url(QString, QString, QString);
51 | };
52 |
--------------------------------------------------------------------------------
/player/include/libvlc/bindings.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "libvlc/types.hpp"
4 |
5 | #include "prelude/c_wrapper.hpp"
6 |
7 | #include
8 | #include
9 | #include
10 |
11 | namespace libvlc {
12 |
13 | struct Instance: CWrapper {
14 | Instance(std::vector);
15 |
16 | using log_cb_t = std::function;
17 |
18 | void set_log_callback(log_cb_t);
19 | void unset_log_callback();
20 |
21 | private:
22 | log_cb_t _cb;
23 | };
24 |
25 | struct Media: CWrapper {
26 | Media(Instance &, const char *);
27 | };
28 |
29 | struct Equalizer: CWrapper {
30 | Equalizer();
31 | };
32 |
33 | struct MediaPlayer: CWrapper {
34 | MediaPlayer(Instance &);
35 |
36 | void set_media(Media &);
37 | void set_renderer(void *);
38 | void play();
39 | void stop();
40 | void set_volume(int);
41 | void set_position(float);
42 |
43 | bool video_filters_enabled();
44 | void enable_video_filters(bool);
45 |
46 | float get_contrast();
47 | void set_contrast(float);
48 | float get_brightness();
49 | void set_brightness(float);
50 | float get_hue();
51 | void set_hue(float);
52 | float get_saturation();
53 | void set_saturation(float);
54 | float get_gamma();
55 | void set_gamma(float);
56 |
57 | std::vector audio_devices();
58 | std::string get_current_device_id();
59 | void set_audio_device(std::string);
60 |
61 | using event_cb_t = std::function;
62 |
63 | void set_event_callback(event_cb_t);
64 |
65 | private:
66 | Equalizer _equalizer;
67 | event_cb_t _cb;
68 |
69 | // For some reason, `libvlc_video_get_adjust` does not work for
70 | // libvlc_adjust_Enable.
71 | // Keeping track of the enabling manually...
72 | bool _adjust_enabled = false;
73 | };
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/player/include/libvlc/event_watcher.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | #include "libvlc/types.hpp"
6 |
7 | #include "prelude/sync.hpp"
8 |
9 | class VLCEventWatcher: public QObject {
10 | Q_OBJECT
11 |
12 | public:
13 | VLCEventWatcher(libvlc::MediaPlayer &, QObject * = nullptr);
14 |
15 | signals:
16 | void new_event(libvlc::Event);
17 |
18 | private:
19 | using EventQueue = sync::Queue;
20 | EventQueue _queue;
21 | };
22 |
--------------------------------------------------------------------------------
/player/include/libvlc/logger.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | #include "libvlc/types.hpp"
6 |
7 | #include "prelude/sync.hpp"
8 |
9 | class VLCLogger: public QObject {
10 | Q_OBJECT
11 |
12 | public:
13 | VLCLogger(libvlc::Instance &, QObject * = nullptr);
14 | ~VLCLogger();
15 |
16 | signals:
17 | void new_log_entry(libvlc::LogEntry);
18 |
19 | private:
20 | libvlc::Instance &_video_context;
21 |
22 | using LogEntryQueue = sync::Queue;
23 | LogEntryQueue _queue;
24 | };
25 |
--------------------------------------------------------------------------------
/player/include/libvlc/types.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 |
6 | // LibVLC uses ssize_t, which is POSIX...
7 | #if defined(_MSC_VER)
8 | #include
9 | using ssize_t = SSIZE_T;
10 | #endif
11 |
12 | // libvlc Forwards
13 | struct libvlc_instance_t;
14 | struct libvlc_media_player_t;
15 | struct libvlc_media_t;
16 | struct libvlc_equalizer_t;
17 | struct libvlc_event_manager_t;
18 | struct vlc_log_t;
19 | //
20 |
21 | namespace libvlc {
22 |
23 | // Bindings forwards
24 | struct Instance;
25 | struct Media;
26 | struct Equalizer;
27 | struct MediaPlayer;
28 | //
29 |
30 | namespace events {
31 | struct Opening { };
32 | struct Playing { };
33 | struct TimeChanged { int64_t new_time; };
34 | struct Buffering { float cache_percent; };
35 | struct Stopped { };
36 | struct EndReached { };
37 | struct EncounteredError { };
38 | struct Unknown { };
39 | }
40 |
41 | using Event = std::variant<
42 | events::Opening,
43 | events::Playing,
44 | events::TimeChanged,
45 | events::Buffering,
46 | events::Stopped,
47 | events::EndReached,
48 | events::EncounteredError,
49 | events::Unknown
50 | >;
51 |
52 | enum class LogLevel {
53 | Debug,
54 | Notice,
55 | Warning,
56 | Error,
57 | Unknown
58 | };
59 |
60 | struct LogEntry {
61 | LogLevel level;
62 | std::string text;
63 | };
64 |
65 | struct AudioDevice {
66 | std::string id;
67 | std::string description;
68 | };
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/player/include/prelude/c_wrapper.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | template
6 | using CDeleter = void (*)(T*);
7 |
8 | template
9 | using CResource = std::unique_ptr>;
10 |
11 | template
12 | struct CWrapper {
13 | CWrapper(T* resource_ptr, CDeleter deleter):
14 | resource(resource_ptr, deleter)
15 | { }
16 |
17 | auto operator &() const { return resource.get(); }
18 | bool init_success() const { return static_cast(resource); }
19 |
20 | private:
21 | using resource_t = CResource;
22 |
23 | resource_t resource;
24 | };
25 |
--------------------------------------------------------------------------------
/player/include/prelude/http.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 |
6 | #include
7 |
8 | class APIClient {
9 | private:
10 | QNetworkAccessManager _http_client;
11 |
12 | auto send_request(const QNetworkRequest &request, const QByteArray &verb) {
13 | using QtPromise::QPromise;
14 |
15 | return QPromise([=](auto& resolve, auto& reject) {
16 | auto reply = _http_client.sendCustomRequest(request, verb);
17 |
18 | QObject::connect(reply, &QNetworkReply::finished, [=]() {
19 | auto error = reply->error();
20 |
21 | if (error == QNetworkReply::NoError)
22 | resolve(reply->readAll());
23 | else
24 | reject(error);
25 |
26 | reply->deleteLater();
27 | });
28 |
29 | });
30 | }
31 |
32 | public:
33 | auto get(const QNetworkRequest &request) {
34 | return send_request(request, "GET");
35 | }
36 |
37 | auto post(const QNetworkRequest &request) {
38 | return send_request(request, "POST");
39 | }
40 |
41 | protected:
42 | template
43 | using response_t = QtPromise::QPromise;
44 | };
45 |
--------------------------------------------------------------------------------
/player/include/prelude/promise.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 |
6 | #include
7 |
8 | struct Cancellable {
9 | void cancel() {
10 | _cancelled = true;
11 | }
12 |
13 | bool cancelled() const {
14 | return _cancelled;
15 | }
16 |
17 | private:
18 | bool _cancelled = false;
19 | };
20 |
21 | struct CancelError: std::future_error {
22 | CancelError(): std::future_error { std::future_errc::broken_promise } { }
23 | };
24 |
25 | template
26 | auto make_cancellable(QtPromise::QPromise promise) {
27 | auto cancel_token = std::make_shared();
28 |
29 | auto tapped_promise = promise.tap([=](auto...) {
30 | if (cancel_token->cancelled())
31 | throw CancelError { };
32 | });
33 |
34 | return std::make_pair(cancel_token, tapped_promise);
35 | }
36 |
37 | using CancelToken = std::shared_ptr;
38 |
--------------------------------------------------------------------------------
/player/include/prelude/sync.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | #include
6 |
7 | namespace sync {
8 |
9 | template
10 | struct Queue {
11 | using Item = T;
12 |
13 | void push(Item && item) {
14 | lock_t lock { _mutex };
15 |
16 | _queue.push(std::move(item));
17 | }
18 |
19 | std::optional try_pop() {
20 | lock_t lock { _mutex };
21 |
22 | if (_queue.empty())
23 | return std::nullopt;
24 |
25 | auto item = std::move(_queue.front());
26 | _queue.pop();
27 |
28 | return std::move(item);
29 | }
30 |
31 | private:
32 | using lock_t = std::unique_lock;
33 |
34 | std::mutex _mutex;
35 | std::queue- _queue;
36 | };
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/player/include/prelude/timer.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | template
6 | void interval(QObject *parent, int ms, Slot && slot) {
7 | auto timer = new QTimer(parent);
8 | QObject::connect(timer, &QTimer::timeout, std::forward(slot));
9 | timer->start(ms);
10 | }
11 |
12 | template
13 | void delayed(QObject *parent, int ms, Slot && slot) {
14 | auto timer = new QTimer(parent);
15 | QObject::connect(timer, &QTimer::timeout, std::forward(slot));
16 | timer->setSingleShot(true);
17 | timer->start(ms);
18 | }
19 |
--------------------------------------------------------------------------------
/player/include/prelude/variant.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | template struct Overloaded: Ts... { using Ts::operator()...; };
6 | template Overloaded(Ts...) -> Overloaded;
7 |
8 | template
9 | constexpr auto match(Variant && variant, Cases &&... cases) {
10 | return std::visit(
11 | Overloaded { std::forward(cases)... },
12 | std::forward(variant)
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/player/include/process/daemon_control.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | namespace daemon_control {
6 | struct Status {
7 | bool running = false;
8 | bool managed = true;
9 | QString version;
10 | };
11 |
12 | bool start();
13 | bool stop();
14 |
15 | Status status();
16 | };
17 |
--------------------------------------------------------------------------------
/player/include/ui/layouts/flow.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | // Adapted from
4 | // http://doc.qt.io/qt-5/qtwidgets-layouts-flowlayout-flowlayout-h.html
5 |
6 | #include
7 | #include
8 | #include
9 |
10 | class FlowLayout: public QLayout {
11 | public:
12 | explicit FlowLayout(QWidget * = nullptr);
13 | ~FlowLayout();
14 |
15 | void addItem(QLayoutItem *) override;
16 | int horizontalSpacing() const;
17 | int verticalSpacing() const;
18 | Qt::Orientations expandingDirections() const override;
19 | bool hasHeightForWidth() const override;
20 | int heightForWidth(int) const override;
21 | int count() const override;
22 | QLayoutItem *itemAt(int) const override;
23 | QSize minimumSize() const override;
24 | void setGeometry(const QRect &) override;
25 | QSize sizeHint() const override;
26 | QLayoutItem *takeAt(int) override;
27 |
28 | private:
29 | int doLayout(const QRect &, bool) const;
30 | int smartSpacing(QStyle::PixelMetric) const;
31 |
32 | QList itemList;
33 | };
34 |
--------------------------------------------------------------------------------
/player/include/ui/layouts/splitter_grid.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 |
6 | class QSplitter;
7 | class QHBoxLayout;
8 |
9 | struct Position {
10 | int row, column;
11 |
12 | Position left(Qt::Orientation orientation = Qt::Vertical) {
13 | return orientation == Qt::Vertical
14 | ? Position { row, column - 1 }
15 | : up();
16 | }
17 | Position right(Qt::Orientation orientation = Qt::Vertical) {
18 | return orientation == Qt::Vertical
19 | ? Position { row, column + 1 }
20 | : down();
21 | }
22 | Position up(Qt::Orientation orientation = Qt::Vertical) {
23 | return orientation == Qt::Vertical
24 | ? Position { row - 1, column }
25 | : left();
26 | }
27 | Position down(Qt::Orientation orientation = Qt::Vertical) {
28 | return orientation == Qt::Vertical
29 | ? Position { row + 1, column }
30 | : right();
31 | }
32 | };
33 |
34 | class SplitterGrid: public QWidget {
35 | public:
36 | SplitterGrid(QWidget * = nullptr);
37 |
38 | void insert_widget(Position, QWidget *);
39 | void remove_widget(QWidget *);
40 | Position widget_position(QWidget *);
41 | QWidget * closest_widget(Position);
42 | void swap(Position, Position);
43 | void rotate();
44 | Qt::Orientation orientation() const;
45 |
46 | private:
47 | Qt::Orientation _main_orientation = Qt::Vertical;
48 |
49 | QHBoxLayout *_layout;
50 | QSplitter *_main_splitter;
51 | std::vector _inner_splitters;
52 |
53 | QSplitter * get_inner(int);
54 | };
55 |
--------------------------------------------------------------------------------
/player/include/ui/main_window.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 |
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 |
12 | #include "ui/layouts/splitter_grid.hpp"
13 |
14 | namespace Ui {
15 | class MainWindow;
16 | }
17 |
18 | namespace libvlc {
19 | struct Instance;
20 | }
21 |
22 | class StreamPane;
23 | class ChatPane;
24 | class VLCLogViewer;
25 | class QStackedWidget;
26 | class QShortcut;
27 |
28 | class TwitchPubSub;
29 |
30 | using MPane = std::variant<
31 | StreamPane *,
32 | ChatPane *
33 | >;
34 |
35 | class MainWindow : public QMainWindow {
36 | public:
37 | MainWindow(libvlc::Instance &, TwitchPubSub &, QWidget * = nullptr);
38 | ~MainWindow();
39 |
40 | StreamPane *add_stream_pane(Position);
41 | ChatPane *add_chat_pane(Position);
42 | void remove_pane(MPane);
43 |
44 | protected:
45 | void changeEvent(QEvent *) override;
46 | void keyPressEvent(QKeyEvent *) override;
47 |
48 | void closeEvent(QCloseEvent *) override;
49 |
50 | private:
51 | std::unique_ptr _ui;
52 |
53 | libvlc::Instance &_video_context;
54 | TwitchPubSub &_pubsub;
55 |
56 | std::unique_ptr _vlc_log_viewer;
57 |
58 | std::vector _panes;
59 | SplitterGrid *_grid;
60 | QStackedWidget *_central_widget;
61 |
62 | Position _zoomed_position;
63 |
64 | std::optional focused_pane();
65 |
66 | void move_focus(Position);
67 | void move_pane(Position, Position);
68 |
69 | void set_fullscreen(bool);
70 | bool is_zoomed();
71 | void zoom(StreamPane *);
72 | void unzoom();
73 |
74 | std::vector>> _shortcuts;
75 |
76 | void setup_shortcuts();
77 | void remap_shortcuts();
78 |
79 | QMenu *_audio_devices_menu;
80 | void setup_audio_devices();
81 |
82 | QAction *_action_stream_zoom;
83 | QAction *_action_fullscreen;
84 | };
85 |
--------------------------------------------------------------------------------
/player/include/ui/native/capabilities.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #ifdef _WIN32
4 | #include "ui/native/win32.hpp"
5 | #elif __APPLE__
6 | #include "ui/native/osx.hpp"
7 | #elif __linux__
8 | #include "ui/native/x11.hpp"
9 | #else
10 | static_assert(false, "Unsupported target");
11 | #endif
12 |
13 | template
14 | static constexpr auto reify_handle(From from) {
15 | constexpr auto from_is_pointer = std::is_pointer_v,
16 | to_is_pointer = std::is_pointer_v;
17 |
18 | if constexpr (from_is_pointer != to_is_pointer)
19 | return reinterpret_cast(from);
20 | else
21 | return static_cast(from);
22 | }
23 |
24 | static constexpr auto to_native_handle(uintptr_t qt_handle) {
25 | return reify_handle(qt_handle);
26 | }
27 |
28 | static constexpr auto from_native_handle(WindowHandle native_handle) {
29 | return reify_handle(native_handle);
30 | }
--------------------------------------------------------------------------------
/player/include/ui/native/osx.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 |
6 | using WindowHandle = void *;
7 | using FindWindowResult = std::set;
8 |
9 | FindWindowResult find_windows_by_title_and_pname(std::string, std::string);
10 |
11 | void toggle_window_borders(WindowHandle, bool);
12 |
13 | void toggle_always_on_top(WindowHandle, bool);
14 |
15 | void sysclose_window(WindowHandle);
16 |
17 | void redraw(WindowHandle);
18 |
19 | void set_transparent(WindowHandle);
20 |
--------------------------------------------------------------------------------
/player/include/ui/native/win32.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | #include
6 |
7 | struct HWND__;
8 |
9 | using WindowHandle = HWND__ *;
10 | using FindWindowResult = std::set;
11 |
12 | FindWindowResult find_windows_by_title_and_pname(std::regex, std::string);
13 |
14 | void toggle_window_borders(WindowHandle, bool);
15 | void toggle_always_on_top(WindowHandle, bool);
16 |
17 | void sysclose_window(WindowHandle);
18 |
19 | void redraw(WindowHandle);
20 |
21 | void set_transparent(WindowHandle);
22 |
--------------------------------------------------------------------------------
/player/include/ui/native/x11.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 |
6 | using WindowHandle = unsigned long;
7 | using FindWindowResult = std::set;
8 |
9 | FindWindowResult find_windows_by_title_and_pname(std::string, std::string);
10 |
11 | void toggle_window_borders(WindowHandle, bool);
12 |
13 | void toggle_always_on_top(WindowHandle, bool);
14 |
15 | void sysclose_window(WindowHandle);
16 |
17 | void redraw(WindowHandle);
18 |
19 | void set_transparent(WindowHandle);
20 |
--------------------------------------------------------------------------------
/player/include/ui/overlays/video_controls.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | #include
6 |
7 | namespace Ui {
8 | class VideoControls;
9 | }
10 |
11 | class QTimer;
12 |
13 | class VideoControls: public QWidget {
14 | Q_OBJECT
15 |
16 | public:
17 | VideoControls(QWidget * = nullptr);
18 | ~VideoControls();
19 |
20 | void set_volume(int);
21 | void set_muted(bool);
22 |
23 | void set_zoomed(bool);
24 | void set_fullscreen(bool);
25 |
26 | void appear();
27 |
28 | void clear_qualities();
29 | void set_qualities(QString, QStringList);
30 |
31 | void set_delay(float);
32 |
33 | protected:
34 | void mousePressEvent(QMouseEvent *) override;
35 | void paintEvent(QPaintEvent *) override;
36 |
37 | signals:
38 | void fast_forward();
39 | void muted_changed(bool);
40 | void volume_changed(int);
41 | void quality_changed(QString);
42 |
43 | void layout_left_requested();
44 | void layout_right_requested();
45 | void zoom_requested(bool);
46 | void fullscreen_requested(bool);
47 |
48 | void browse_requested();
49 | void remove_requested();
50 |
51 | private:
52 | void set_volume_icon();
53 | void set_zoomed_icon();
54 | void set_fullscreen_icon();
55 |
56 | std::unique_ptr _ui;
57 |
58 | QTimer *_appearTimer;
59 |
60 | bool _muted = false;
61 | bool _fullscreen = false;
62 | bool _zoomed = false;
63 | };
64 |
--------------------------------------------------------------------------------
/player/include/ui/overlays/video_details.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 |
6 | #include
7 |
8 | #include "api/twitch.hpp"
9 |
10 | namespace Ui {
11 | class StreamDetails;
12 | }
13 |
14 | class VideoDetails: public QWidget {
15 | public:
16 | VideoDetails(QWidget * = nullptr);
17 | ~VideoDetails();
18 |
19 | void show_state(const QString &);
20 | void set_buffering(bool);
21 | void show_stream_details();
22 |
23 | void set_channel(const QString &);
24 |
25 | void hide_stream_details();
26 |
27 | protected:
28 | void paintEvent(QPaintEvent *) override;
29 | void mouseReleaseEvent(QMouseEvent *) override;
30 |
31 | private:
32 | QString _state_text;
33 | QTimer *_state_timer;
34 | QFont _state_text_font;
35 | bool _show_state_text = false;
36 |
37 | QImage _spinner;
38 | QTimer *_spinner_timer;
39 | int _spinner_angle = 0;
40 | bool _show_spinner = false;
41 |
42 | std::unique_ptr _stream_details_widget;
43 | std::unique_ptr _stream_details_ui;
44 | QString _channel;
45 | QTimer *_stream_details_timer;
46 | bool _show_stream_details = false;
47 | bool _has_valid_stream_details = false;
48 |
49 | QNetworkAccessManager *_http_client;
50 |
51 | void draw_state_text();
52 | void draw_spinner();
53 | void draw_stream_details();
54 |
55 | void fetch_channel_details();
56 | void fetch_channel_logo(const QString &);
57 |
58 | TwitchAPI _api;
59 | };
60 |
--------------------------------------------------------------------------------
/player/include/ui/tools/about_dialog.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | #include
6 |
7 | namespace Ui {
8 | class AboutDialog;
9 | }
10 |
11 | class AboutDialog: public QDialog {
12 | public:
13 | AboutDialog(QWidget * = nullptr);
14 | ~AboutDialog();
15 |
16 | private:
17 | std::unique_ptr _ui;
18 | };
19 |
--------------------------------------------------------------------------------
/player/include/ui/tools/options_dialog.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | #include
6 | #include
7 | #include
8 |
9 | namespace Ui {
10 | class OptionsDialog;
11 | }
12 |
13 | class QKeySequenceEdit;
14 |
15 | class TwitchPubSub;
16 |
17 | class OptionsDialog: public QDialog {
18 | Q_OBJECT
19 |
20 | public:
21 | OptionsDialog(TwitchPubSub &, QWidget * = nullptr);
22 | ~OptionsDialog();
23 |
24 | signals:
25 | void settings_changed();
26 |
27 | private:
28 | void load_settings();
29 | void save_settings();
30 |
31 | void update_daemon(QString, std::function);
32 |
33 | TwitchPubSub &_pubsub;
34 |
35 | std::unique_ptr _ui;
36 | std::vector> _keybind_edits;
37 | };
38 |
--------------------------------------------------------------------------------
/player/include/ui/tools/video_filters.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | #include
6 |
7 | namespace Ui {
8 | class VideoFilters;
9 | };
10 |
11 | namespace libvlc {
12 | struct MediaPlayer;
13 | }
14 |
15 | class VideoFilters: public QWidget {
16 | public:
17 | VideoFilters(libvlc::MediaPlayer &, QWidget * = nullptr);
18 |
19 | private:
20 | std::unique_ptr _ui;
21 | };
22 |
--------------------------------------------------------------------------------
/player/include/ui/tools/vlc_log_viewer.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 |
8 | #include
9 | #include
10 | #include
11 |
12 | #include "libvlc/bindings.hpp"
13 |
14 | namespace Ui {
15 | class VLCLogViewer;
16 | }
17 |
18 | class VLCLogger;
19 |
20 | class VLCLogItemModel: public QAbstractItemModel {
21 | public:
22 | VLCLogItemModel(QObject * = nullptr);
23 |
24 | QModelIndex index(int, int, const QModelIndex & = QModelIndex()) const override;
25 | QModelIndex parent(const QModelIndex &) const override;
26 | int rowCount(const QModelIndex & = QModelIndex()) const override;
27 | int columnCount(const QModelIndex & = QModelIndex()) const override;
28 | QVariant data(const QModelIndex &, int = Qt::DisplayRole) const override;
29 |
30 | void add_log_entry(libvlc::LogEntry);
31 |
32 | struct LogEntry {
33 | QDateTime stamp;
34 | libvlc::LogEntry data;
35 | };
36 | std::vector entries;
37 | };
38 |
39 | class VLCLogItemFilter: public QSortFilterProxyModel {
40 | public:
41 | VLCLogItemFilter(VLCLogItemModel &, QObject * = nullptr);
42 |
43 | bool filterAcceptsRow(int, const QModelIndex &) const override;
44 |
45 | void toggle_level(libvlc::LogLevel, bool);
46 | private:
47 | std::set _filtered_levels;
48 | VLCLogItemModel &_model;
49 | };
50 |
51 | class VLCLogViewer: public QWidget {
52 | public:
53 | VLCLogViewer(libvlc::Instance &, QWidget * = nullptr);
54 | ~VLCLogViewer();
55 |
56 | private:
57 | std::unique_ptr _ui;
58 |
59 | VLCLogItemModel *_item_model;
60 | VLCLogItemFilter *_filter_proxy_model;
61 | VLCLogger *_logger;
62 | std::mutex _log_mutex;
63 | };
64 |
--------------------------------------------------------------------------------
/player/include/ui/tray.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 |
6 | #include
7 |
8 | class TwitchPubSub;
9 |
10 | class SystemTray: public QSystemTrayIcon {
11 | Q_OBJECT
12 |
13 | public:
14 | SystemTray(TwitchPubSub &);
15 |
16 | signals:
17 | void channel_open_requested(QString);
18 |
19 | private:
20 | TwitchPubSub &_pubsub;
21 |
22 | std::optional channel_to_play;
23 |
24 | QMenu _menu;
25 | };
26 |
--------------------------------------------------------------------------------
/player/include/ui/utils/event_notifier.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 | #include
5 |
6 | #include
7 |
8 | class EventNotifier: public QObject {
9 | Q_OBJECT
10 |
11 | public:
12 | EventNotifier(std::initializer_list, QObject * = nullptr);
13 |
14 | signals:
15 | void new_event(const QEvent &);
16 |
17 | protected:
18 | bool eventFilter(QObject *, QEvent *) override;
19 |
20 | private:
21 | std::unordered_set _monitored_events;
22 | };
23 |
--------------------------------------------------------------------------------
/player/include/ui/widgets/chat_pane.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | #include
6 |
7 | class QHBoxLayout;
8 | class ForeignWidget;
9 |
10 | class ChatPane: public QWidget {
11 | Q_OBJECT
12 |
13 | public:
14 | ChatPane(QWidget * = nullptr);
15 | ~ChatPane();
16 |
17 | private:
18 | QHBoxLayout *_layout;
19 | ForeignWidget *_chat;
20 | };
21 |
--------------------------------------------------------------------------------
/player/include/ui/widgets/foreign_widget.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | #include
6 |
7 | #include "ui/native/capabilities.hpp"
8 |
9 | class QHBoxLayout;
10 |
11 | class ForeignWidget: public QWidget {
12 | public:
13 | ForeignWidget(QWidget * = nullptr);
14 | ~ForeignWidget();
15 |
16 | void grab(WindowHandle);
17 | void redraw();
18 |
19 | private:
20 | std::optional _foreign_win_ptr;
21 | std::optional _container;
22 | QHBoxLayout *_layout;
23 |
24 | void release_window();
25 | };
26 |
--------------------------------------------------------------------------------
/player/include/ui/widgets/stream_card.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | #include "api/twitch.hpp"
6 |
7 | namespace Ui {
8 | class StreamCard;
9 | }
10 |
11 | class QHBoxLayout;
12 | class QLabel;
13 |
14 | class StreamCard: public QWidget {
15 | Q_OBJECT
16 |
17 | public:
18 | StreamCard(StreamData, QWidget * = nullptr);
19 | ~StreamCard();
20 |
21 | protected:
22 | void mousePressEvent(QMouseEvent *) override;
23 |
24 | signals:
25 | void clicked(QString);
26 |
27 | private:
28 | std::unique_ptr _ui;
29 |
30 | QWidget *_uptime_widget;
31 | QHBoxLayout *_uptime_layout;
32 | QLabel *_uptime_logo;
33 | QLabel *_uptime_label;
34 |
35 | StreamData _data;
36 | };
37 |
--------------------------------------------------------------------------------
/player/include/ui/widgets/stream_pane.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | #include
6 |
7 | class QHBoxLayout;
8 | class StreamPicker;
9 | class StreamWidget;
10 |
11 | namespace libvlc {
12 | struct Instance;
13 | }
14 |
15 | class StreamPane: public QWidget {
16 | Q_OBJECT
17 |
18 | public:
19 | StreamPane(libvlc::Instance &, QWidget * = nullptr);
20 | ~StreamPane();
21 |
22 | void play(QString, QString = QString());
23 |
24 | StreamWidget *stream() const;
25 |
26 | protected:
27 | void paintEvent(QPaintEvent *) override;
28 | void focusOutEvent(QFocusEvent *) override;
29 | void focusInEvent(QFocusEvent *) override;
30 |
31 | signals:
32 | void remove_requested();
33 | void zoom_requested(bool);
34 | void fullscreen_requested(bool);
35 |
36 | private:
37 | libvlc::Instance & _video_ctx;
38 |
39 | QHBoxLayout *_layout;
40 | std::unique_ptr _picker;
41 | std::unique_ptr _stream;
42 |
43 | void setup_picker();
44 | void setup_stream();
45 | };
46 |
--------------------------------------------------------------------------------
/player/include/ui/widgets/stream_picker.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | #include
6 |
7 | #include "api/twitch.hpp"
8 |
9 | #include "prelude/promise.hpp"
10 |
11 | namespace Ui {
12 | class StreamPicker;
13 | }
14 |
15 | class StreamPicker: public QWidget {
16 | Q_OBJECT
17 |
18 | public:
19 | StreamPicker(QWidget * = nullptr);
20 | ~StreamPicker();
21 |
22 | protected:
23 | void focusInEvent(QFocusEvent *) override;
24 |
25 | signals:
26 | void stream_picked(QString, QString);
27 |
28 | private:
29 | std::unique_ptr _ui;
30 |
31 | QWidget *_channels_stream_presenter;
32 | QWidget *_followed_stream_presenter;
33 |
34 | TwitchAPI _api;
35 | std::unordered_map current_queries;
36 |
37 | void fetch_streams(TwitchAPI::streams_response_t, QWidget *);
38 | void present_streams(QWidget *, QList);
39 |
40 | void channel_picked(QString);
41 | };
42 |
--------------------------------------------------------------------------------
/player/include/ui/widgets/stream_widget.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include
4 |
5 | class VideoWidget;
6 | class ForeignWidget;
7 |
8 | namespace libvlc {
9 | struct Instance;
10 | }
11 |
12 | class QSplitter;
13 | class QHBoxLayout;
14 |
15 | enum class ChatPosition { Left, Right };
16 |
17 | class StreamWidget: public QWidget {
18 | public:
19 | StreamWidget(libvlc::Instance &, QWidget * = nullptr);
20 | ~StreamWidget();
21 |
22 | void play(QString, QString);
23 |
24 | void reposition_chat(ChatPosition);
25 | void resize_chat(ChatPosition);
26 |
27 | VideoWidget *video() const;
28 | ForeignWidget *chat() const;
29 |
30 | private:
31 | QSplitter *_splitter;
32 | QHBoxLayout *_layout;
33 | VideoWidget *_video;
34 | ForeignWidget *_chat;
35 |
36 | int _chat_size, _video_size;
37 | ChatPosition _chat_position;
38 | };
39 |
--------------------------------------------------------------------------------
/player/include/ui/widgets/video_widget.hpp:
--------------------------------------------------------------------------------
1 | #pragma once
2 |
3 | #include "libvlc/bindings.hpp"
4 |
5 | #include "api/twitchd.hpp"
6 |
7 | #include
8 | #include
9 |
10 | class VideoControls;
11 | class VideoDetails;
12 | class VLCEventWatcher;
13 |
14 | class VideoWidget: public QWidget {
15 | public:
16 | VideoWidget(libvlc::Instance &, QWidget * = nullptr);
17 | ~VideoWidget();
18 |
19 | void play(QString, QString);
20 |
21 | int volume() const;
22 | void set_volume(int);
23 |
24 | bool muted() const;
25 | void set_muted(bool);
26 |
27 | void fast_forward();
28 |
29 | void hint_layout_change();
30 |
31 | libvlc::MediaPlayer & media_player();
32 |
33 | VideoControls & controls() const;
34 |
35 | protected:
36 | void wheelEvent(QWheelEvent *) override;
37 | void resizeEvent(QResizeEvent *) override;
38 | void showEvent(QShowEvent *) override;
39 | void mousePressEvent(QMouseEvent *) override;
40 | void mouseMoveEvent(QMouseEvent *) override;
41 |
42 | private:
43 | libvlc::Instance & _instance;
44 | libvlc::MediaPlayer _media_player;
45 | std::optional _media;
46 |
47 | VideoDetails *_details;
48 | VideoControls *_controls;
49 |
50 | VLCEventWatcher *_event_watcher;
51 |
52 | int _vol;
53 | bool _muted;
54 |
55 | QPoint _last_drag_position;
56 |
57 | TwitchdAPI _api;
58 |
59 | QString _current_channel, _current_quality, _current_meta_key;
60 | std::optional _current_metadata;
61 |
62 | QTimer *_retry_timer;
63 |
64 | void update_overlay_position();
65 |
66 | };
67 |
--------------------------------------------------------------------------------
/player/resources/browse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Globidev/twitch-player/062306685e182c422980d57fe7ba9fbdee141c37/player/resources/browse.png
--------------------------------------------------------------------------------
/player/resources/clock.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/player/resources/dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Globidev/twitch-player/062306685e182c422980d57fe7ba9fbdee141c37/player/resources/dashboard.png
--------------------------------------------------------------------------------
/player/resources/fast_forward.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Globidev/twitch-player/062306685e182c422980d57fe7ba9fbdee141c37/player/resources/fast_forward.png
--------------------------------------------------------------------------------
/player/resources/fullscreen_enter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Globidev/twitch-player/062306685e182c422980d57fe7ba9fbdee141c37/player/resources/fullscreen_enter.png
--------------------------------------------------------------------------------
/player/resources/fullscreen_exit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Globidev/twitch-player/062306685e182c422980d57fe7ba9fbdee141c37/player/resources/fullscreen_exit.png
--------------------------------------------------------------------------------
/player/resources/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Globidev/twitch-player/062306685e182c422980d57fe7ba9fbdee141c37/player/resources/icon.ico
--------------------------------------------------------------------------------
/player/resources/joystick.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/player/resources/kappa.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Globidev/twitch-player/062306685e182c422980d57fe7ba9fbdee141c37/player/resources/kappa.png
--------------------------------------------------------------------------------
/player/resources/layout_left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Globidev/twitch-player/062306685e182c422980d57fe7ba9fbdee141c37/player/resources/layout_left.png
--------------------------------------------------------------------------------
/player/resources/layout_right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Globidev/twitch-player/062306685e182c422980d57fe7ba9fbdee141c37/player/resources/layout_right.png
--------------------------------------------------------------------------------
/player/resources/player.qrc:
--------------------------------------------------------------------------------
1 |
2 |
3 | clock.svg
4 | dashboard.png
5 | unzoom.png
6 | zoom.png
7 | fullscreen_enter.png
8 | browse.png
9 | fullscreen_exit.png
10 | layout_left.png
11 | layout_right.png
12 | remove.png
13 | joystick.svg
14 | viewers.svg
15 | fast_forward.png
16 | icon.ico
17 | volume_off.png
18 | volume_on.png
19 |
20 |
21 | kappa.png
22 |
23 |
24 |
--------------------------------------------------------------------------------
/player/resources/remove.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Globidev/twitch-player/062306685e182c422980d57fe7ba9fbdee141c37/player/resources/remove.png
--------------------------------------------------------------------------------
/player/resources/unzoom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Globidev/twitch-player/062306685e182c422980d57fe7ba9fbdee141c37/player/resources/unzoom.png
--------------------------------------------------------------------------------
/player/resources/viewers.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/player/resources/volume_off.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Globidev/twitch-player/062306685e182c422980d57fe7ba9fbdee141c37/player/resources/volume_off.png
--------------------------------------------------------------------------------
/player/resources/volume_on.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Globidev/twitch-player/062306685e182c422980d57fe7ba9fbdee141c37/player/resources/volume_on.png
--------------------------------------------------------------------------------
/player/resources/zoom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Globidev/twitch-player/062306685e182c422980d57fe7ba9fbdee141c37/player/resources/zoom.png
--------------------------------------------------------------------------------
/player/scripts/win32/build.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | cd target
3 | set CL=/MP
4 | nmake %1
5 |
--------------------------------------------------------------------------------
/player/scripts/win32/qmake.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | cd target
3 | if "%1" == "deploy" (
4 | %QT_STATIC_BIN_PATH%\qmake.exe ..
5 | ) else (
6 | qmake ..
7 | )
8 |
--------------------------------------------------------------------------------
/player/src/api/oauth.cpp:
--------------------------------------------------------------------------------
1 | #include "api/oauth.hpp"
2 |
3 | #include "constants.hpp"
4 |
5 | #include
6 | #include
7 | #include
8 |
9 | #include
10 | #include
11 | #include
12 | #include
13 |
14 | #include
15 | #include
16 | #include
17 |
18 | #include
19 |
20 | #include
21 |
22 | static auto access_code_url(const QByteArray & code) {
23 | QUrl url { "https://id.twitch.tv/oauth2/token" };
24 |
25 | QUrlQuery url_query;
26 | url_query.addQueryItem("client_id", constants::TWITCHD_CLIENT_ID);
27 | url_query.addQueryItem("client_secret", "8mpn5c01v3k5fdi094x6275cn18du7");
28 | url_query.addQueryItem("code", code);
29 | url_query.addQueryItem("grant_type", "authorization_code");
30 | url_query.addQueryItem("redirect_uri", constants::OAUTH_REDIRECT_URI);
31 | url.setQuery(url_query);
32 |
33 | return url;
34 | }
35 |
36 | static auto refresh_token_url(const QString & refresh_token) {
37 | QUrl url { "https://id.twitch.tv/oauth2/token" };
38 |
39 | QUrlQuery url_query;
40 | url_query.addQueryItem("grant_type", "refresh_token");
41 | url_query.addQueryItem("refresh_token", refresh_token);
42 | url_query.addQueryItem("client_id", constants::TWITCHD_CLIENT_ID);
43 | url_query.addQueryItem("client_secret", "8mpn5c01v3k5fdi094x6275cn18du7");
44 | url.setQuery(url_query);
45 |
46 | return url;
47 | }
48 |
49 | static auto authorize_url() {
50 | QUrl url { "https://id.twitch.tv/oauth2/authorize" };
51 |
52 | QUrlQuery url_query;
53 | url_query.addQueryItem("client_id", constants::TWITCHD_CLIENT_ID);
54 | url_query.addQueryItem("redirect_uri", constants::OAUTH_REDIRECT_URI);
55 | url_query.addQueryItem("response_type", "code");
56 | url_query.addQueryItem("scope", constants::OAUTH_SCOPES);
57 | url.setQuery(url_query);
58 |
59 | return url;
60 | }
61 |
62 | OAuth::OAuth(QObject *parent):
63 | QObject(parent),
64 | _local_server(new QTcpServer(this))
65 | {
66 | QObject::connect(_local_server, &QTcpServer::newConnection, [=] {
67 | auto client = _local_server->nextPendingConnection();
68 | QObject::connect(client, &QTcpSocket::readyRead, [=] {
69 | while (client->canReadLine()) {
70 | auto line = client->readLine();
71 | auto code_param_index = line.indexOf("GET /?code=");
72 |
73 | if (code_param_index != -1) {
74 | auto code = line.mid(code_param_index + 11, 30);
75 | fetch_token(access_code_url(code));
76 | }
77 |
78 | if (client->atEnd()) {
79 | client->write("HTTP/1.1 200 OK\r\n\r\n
OK
");
80 | client->close();
81 | }
82 | }
83 | });
84 | });
85 |
86 | _local_server->listen(
87 | QHostAddress::LocalHost,
88 | constants::OAUTH_REDIRECT_URI_PORT
89 | );
90 | }
91 |
92 | QPromise OAuth::query_token() {
93 | QSettings settings;
94 |
95 | auto refresh_token = settings
96 | .value(constants::settings::oauth::REFRESH_TOKEN_KEY)
97 | .toString();
98 |
99 | if (!refresh_token.isEmpty())
100 | fetch_token(refresh_token_url(refresh_token));
101 | else
102 | QDesktopServices::openUrl(authorize_url());
103 |
104 | return QPromise([=](const auto & resolve, auto) {
105 | QObject::connect(this, &OAuth::token_ready, [=](auto token) {
106 | resolve(token);
107 | });
108 | });
109 | }
110 |
111 | void OAuth::save_token_data(const QByteArray &raw_data) {
112 | using namespace constants::settings::oauth;
113 |
114 | auto json_data = QJsonDocument::fromJson(raw_data).object();
115 |
116 | auto access_token = json_data["access_token"].toString();
117 | auto refresh_token = json_data["refresh_token"].toString();
118 |
119 | QSettings settings;
120 |
121 | settings.setValue(ACCESS_TOKEN_KEY, access_token);
122 | settings.setValue(REFRESH_TOKEN_KEY, refresh_token);
123 |
124 | emit token_ready(access_token);
125 | }
126 |
127 | void OAuth::fetch_token(const QUrl &url) {
128 | auto http_client = new QNetworkAccessManager(this);
129 | auto token_reply = http_client->post(QNetworkRequest { url }, QByteArray());
130 |
131 | QObject::connect(token_reply, &QNetworkReply::finished, [=] {
132 | auto status = token_reply
133 | ->attribute(QNetworkRequest::HttpStatusCodeAttribute)
134 | .toInt();
135 |
136 | if (status == 200) {
137 | auto token_data = token_reply->readAll();
138 | save_token_data(token_data);
139 | }
140 |
141 | token_reply->deleteLater();
142 | });
143 | }
144 |
--------------------------------------------------------------------------------
/player/src/libvlc/event_watcher.cpp:
--------------------------------------------------------------------------------
1 | #include "libvlc/event_watcher.hpp"
2 |
3 | #include "libvlc/bindings.hpp"
4 |
5 | #include "prelude/timer.hpp"
6 |
7 | VLCEventWatcher::VLCEventWatcher(libvlc::MediaPlayer & mp, QObject *parent):
8 | QObject(parent)
9 | {
10 | mp.set_event_callback([this](auto event) {
11 | _queue.push(std::move(event));
12 | });
13 |
14 | auto poll_events = [this] {
15 | auto opt_event = _queue.try_pop();
16 | while (opt_event) {
17 | emit new_event(std::move(*opt_event));
18 | opt_event = _queue.try_pop();
19 | }
20 | };
21 |
22 | interval(this, 250, poll_events);
23 | }
24 |
--------------------------------------------------------------------------------
/player/src/libvlc/logger.cpp:
--------------------------------------------------------------------------------
1 | #include "libvlc/logger.hpp"
2 |
3 | #include "libvlc/bindings.hpp"
4 |
5 | #include "prelude/timer.hpp"
6 |
7 | VLCLogger::VLCLogger(libvlc::Instance &video_context, QObject *parent):
8 | QObject(parent),
9 | _video_context(video_context)
10 | {
11 | video_context.set_log_callback([this](libvlc::LogEntry entry) {
12 | _queue.push(std::move(entry));
13 | });
14 |
15 | auto poll_entries = [this] {
16 | auto opt_entry = _queue.try_pop();
17 | while (opt_entry) {
18 | emit new_log_entry(std::move(*opt_entry));
19 | opt_entry = _queue.try_pop();
20 | }
21 | };
22 |
23 | interval(this, 250, poll_entries);
24 | }
25 |
26 | VLCLogger::~VLCLogger() {
27 | _video_context.unset_log_callback();
28 | }
29 |
--------------------------------------------------------------------------------
/player/src/process/daemon_control.cpp:
--------------------------------------------------------------------------------
1 | #include "process/daemon_control.hpp"
2 |
3 | #include "api/twitchd.hpp"
4 |
5 | #include "constants.hpp"
6 |
7 | #include
8 | #include
9 |
10 | struct DaemonSettings {
11 | QString host;
12 | quint16 port;
13 |
14 | QString client_id;
15 |
16 | quint32 cache_timeout_s;
17 | quint32 playlist_fetch_interval_ms;
18 | quint32 player_inactive_timeout_s;
19 | quint32 player_fetch_timeout_s;
20 | quint32 player_video_chunks_size;
21 | quint32 player_max_sink_buffer_size;
22 | };
23 |
24 | static DaemonSettings load_settings() {
25 | using namespace constants::settings::daemon;
26 | QSettings settings;
27 |
28 | return DaemonSettings {
29 | settings.value(KEY_HOST_MANAGED, DEFAULT_HOST_MANAGED).toString(),
30 | settings.value(KEY_PORT_MANAGED, DEFAULT_PORT_MANAGED).value(),
31 |
32 | constants::TWITCHD_CLIENT_ID,
33 |
34 | settings.value(KEY_CACHE_TIMEOUT, DEFAULT_CACHE_TIMEOUT).value(),
35 | settings.value(KEY_PLAYLIST_FETCH_INTERVAL, DEFAULT_PLAYLIST_FETCH_INTERVAL).value(),
36 | settings.value(KEY_PLAYER_INACTIVE_TIMEOUT, DEFAULT_PLAYER_INACTIVE_TIMEOUT).value(),
37 | settings.value(KEY_PLAYER_FETCH_TIMEOUT, DEFAULT_PLAYER_FETCH_TIMEOUT).value(),
38 | settings.value(KEY_PLAYER_VIDEO_CHUNKS_SIZE, DEFAULT_PLAYER_VIDEO_CHUNKS_SIZE).value(),
39 | settings.value(KEY_PLAYER_MAX_SINK_BUFFER_SIZE, DEFAULT_PLAYER_MAX_SINK_BUFFER_SIZE).value(),
40 | };
41 | }
42 |
43 | namespace daemon_control {
44 |
45 | bool start() {
46 | auto settings = load_settings();
47 |
48 | auto arguments = QStringList()
49 | << "--client-id" << settings.client_id
50 | << "--host" << settings.host
51 | << "--port" << QString::number(settings.port)
52 | << "--index-cache-timeout" << QString("%1s").arg(settings.cache_timeout_s)
53 | << "--playlist-fetch-interval" << QString("%1ms").arg(settings.playlist_fetch_interval_ms)
54 | << "--player-inactive-timeout" << QString("%1s").arg(settings.player_inactive_timeout_s)
55 | << "--player-fetch-timeout" << QString("%1s").arg(settings.player_fetch_timeout_s)
56 | << "--player-video-chunks-size" << QString("%1").arg(settings.player_video_chunks_size)
57 | << "--player-max-sink-buffer-size" << QString("%1").arg(settings.player_max_sink_buffer_size)
58 | ;
59 |
60 | return QProcess::startDetached(constants::TWITCHD_PATH, arguments);
61 | }
62 |
63 | bool stop() {
64 | bool stopped = false;
65 |
66 | TwitchdAPI api;
67 |
68 | auto quit_response = api.daemon_quit()
69 | .tap([&] {
70 | stopped = true;
71 | });
72 |
73 | quit_response.timeout(2000).wait();
74 |
75 | return stopped;
76 | }
77 |
78 | Status status() {
79 | using namespace constants::settings::daemon;
80 | QSettings settings;
81 |
82 | Status status;
83 | TwitchdAPI api;
84 |
85 | status.managed = settings.value(KEY_MANAGED, DEFAULT_MANAGED).toBool();
86 |
87 | auto version_response = api.daemon_version()
88 | .tap([&](QString version) {
89 | status.running = true;
90 | status.version = version;
91 | });
92 |
93 | version_response.timeout(2000).wait();
94 |
95 | return status;
96 | }
97 |
98 | }
99 |
--------------------------------------------------------------------------------
/player/src/ui/layouts/flow.cpp:
--------------------------------------------------------------------------------
1 | #include "ui/layouts/flow.hpp"
2 |
3 | #include
4 |
5 | FlowLayout::FlowLayout(QWidget *parent):
6 | QLayout(parent)
7 | {
8 | setContentsMargins(-1, -1, -1, -1);
9 | }
10 |
11 | FlowLayout::~FlowLayout() {
12 | QLayoutItem *item;
13 |
14 | while ((item = takeAt(0)))
15 | delete item;
16 | }
17 |
18 | void FlowLayout::addItem(QLayoutItem *item) {
19 | itemList.append(item);
20 | }
21 |
22 | int FlowLayout::horizontalSpacing() const {
23 | return smartSpacing(QStyle::PM_LayoutHorizontalSpacing);
24 | }
25 |
26 | int FlowLayout::verticalSpacing() const {
27 | return smartSpacing(QStyle::PM_LayoutVerticalSpacing);
28 | }
29 |
30 | int FlowLayout::count() const {
31 | return itemList.size();
32 | }
33 |
34 | QLayoutItem *FlowLayout::itemAt(int index) const {
35 | return itemList.value(index);
36 | }
37 |
38 | QLayoutItem *FlowLayout::takeAt(int index) {
39 | if (index >= 0 && index < itemList.size())
40 | return itemList.takeAt(index);
41 | else
42 | return nullptr;
43 | }
44 |
45 | Qt::Orientations FlowLayout::expandingDirections() const {
46 | return 0;
47 | }
48 |
49 | bool FlowLayout::hasHeightForWidth() const {
50 | return true;
51 | }
52 |
53 | int FlowLayout::heightForWidth(int width) const {
54 | int height = doLayout(QRect(0, 0, width, 0), true);
55 | return height;
56 | }
57 |
58 | void FlowLayout::setGeometry(const QRect &rect) {
59 | QLayout::setGeometry(rect);
60 | doLayout(rect, false);
61 | }
62 |
63 | QSize FlowLayout::sizeHint() const {
64 | return minimumSize();
65 | }
66 |
67 | QSize FlowLayout::minimumSize() const {
68 | QSize size;
69 |
70 | for (auto item: itemList)
71 | size = size.expandedTo(item->minimumSize());
72 |
73 | size += QSize(2*margin(), 2*margin());
74 |
75 | return size;
76 | }
77 |
78 | int FlowLayout::doLayout(const QRect &rect, bool testOnly) const {
79 | int left, top, right, bottom;
80 | getContentsMargins(&left, &top, &right, &bottom);
81 | QRect effectiveRect = rect.adjusted(+left, +top, -right, -bottom);
82 | int x = effectiveRect.x();
83 | int y = effectiveRect.y();
84 | int lineHeight = 0;
85 |
86 | for (auto item: itemList) {
87 | QWidget *wid = item->widget();
88 | int spaceX = horizontalSpacing();
89 | if (spaceX == -1)
90 | spaceX = wid->style()->layoutSpacing(
91 | QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Horizontal);
92 | int spaceY = verticalSpacing();
93 | if (spaceY == -1)
94 | spaceY = wid->style()->layoutSpacing(
95 | QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Vertical);
96 | int nextX = x + item->sizeHint().width() + spaceX;
97 | if (nextX - spaceX > effectiveRect.right() && lineHeight > 0) {
98 | x = effectiveRect.x();
99 | y = y + lineHeight + spaceY;
100 | nextX = x + item->sizeHint().width() + spaceX;
101 | lineHeight = 0;
102 | }
103 |
104 | if (!testOnly)
105 | item->setGeometry(QRect(QPoint(x, y), item->sizeHint()));
106 |
107 | x = nextX;
108 | lineHeight = qMax(lineHeight, item->sizeHint().height());
109 | }
110 | return y + lineHeight - rect.y() + bottom;
111 | }
112 |
113 | int FlowLayout::smartSpacing(QStyle::PixelMetric pm) const {
114 | QObject *parent = this->parent();
115 | if (!parent) {
116 | return -1;
117 | } else if (parent->isWidgetType()) {
118 | QWidget *pw = static_cast(parent);
119 | return pw->style()->pixelMetric(pm, 0, pw);
120 | } else {
121 | return static_cast(parent)->spacing();
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/player/src/ui/layouts/splitter_grid.cpp:
--------------------------------------------------------------------------------
1 | #include "ui/layouts/splitter_grid.hpp"
2 |
3 | #include
4 | #include
5 |
6 | static auto equal_sizes(int size) {
7 | QList sizes;
8 | for (int i = 0; i < size; ++i)
9 | sizes << 100;
10 | return sizes;
11 | }
12 |
13 | static auto invert(Qt::Orientation orientation) {
14 | return orientation == Qt::Vertical
15 | ? Qt::Horizontal
16 | : Qt::Vertical;
17 | }
18 |
19 | SplitterGrid::SplitterGrid(QWidget *parent):
20 | QWidget(parent),
21 | _layout(new QHBoxLayout(this)),
22 | _main_splitter(new QSplitter(_main_orientation, this))
23 | {
24 | _layout->setContentsMargins(QMargins());
25 | _layout->addWidget(_main_splitter);
26 |
27 | setLayout(_layout);
28 | }
29 |
30 | void SplitterGrid::insert_widget(Position pos, QWidget *widget) {
31 | auto inner_splitter = get_inner(pos.row);
32 | inner_splitter->insertWidget(pos.column, widget);
33 | inner_splitter->setSizes(equal_sizes(inner_splitter->count()));
34 | }
35 |
36 | void SplitterGrid::remove_widget(QWidget *widget) {
37 | auto pos = widget_position(widget);
38 |
39 | if (static_cast(pos.row) < _inner_splitters.size()) {
40 | auto inner_splitter_it = _inner_splitters.begin();
41 | std::advance(inner_splitter_it, pos.row);
42 | if ((*inner_splitter_it)->count() == 1) {
43 | (*inner_splitter_it)->deleteLater();
44 | _inner_splitters.erase(inner_splitter_it);
45 | }
46 | }
47 | }
48 |
49 | Position SplitterGrid::widget_position(QWidget *widget) {
50 | for (int row = 0; static_cast(row) < _inner_splitters.size(); ++row)
51 | for (int col = 0; col < _inner_splitters[row]->count(); ++col)
52 | if (_inner_splitters[row]->widget(col) == widget)
53 | return { row, col };
54 |
55 | return { 0, 0 };
56 | }
57 |
58 | QWidget * SplitterGrid::closest_widget(Position pos) {
59 | if (_inner_splitters.empty())
60 | return nullptr;
61 |
62 | auto max_row = static_cast(_inner_splitters.size() - 1);
63 | auto row = std::max(std::min(pos.row, max_row), 0);
64 | auto inner_splitter = _inner_splitters[row];
65 | auto col = std::max(std::min(pos.column, inner_splitter->count() - 1), 0);
66 |
67 | return inner_splitter->widget(col);
68 | }
69 |
70 | void SplitterGrid::swap(Position from, Position to) {
71 | auto from_w = closest_widget(from);
72 | auto to_w = closest_widget(to);
73 | if (from_w && from_w != to_w) {
74 | auto actual_from = widget_position(from_w);
75 | auto actual_to = widget_position(to_w);
76 | auto splitter_from = _inner_splitters[actual_from.row];
77 | auto splitter_to = _inner_splitters[actual_to.row];
78 | auto splitter_from_sizes = splitter_from->sizes();
79 | auto splitter_to_sizes = splitter_to->sizes();
80 | splitter_from->replaceWidget(actual_from.column, to_w);
81 | splitter_to->insertWidget(actual_to.column, from_w);
82 | splitter_from->setSizes(splitter_from_sizes);
83 | splitter_to->setSizes(splitter_to_sizes);
84 | }
85 | }
86 |
87 | void SplitterGrid::rotate() {
88 | _main_orientation = invert(_main_orientation);
89 | _main_splitter->setOrientation(_main_orientation);
90 | for (auto inner_splitter: _inner_splitters)
91 | inner_splitter->setOrientation(invert(_main_orientation));
92 | }
93 |
94 | Qt::Orientation SplitterGrid::orientation() const {
95 | return _main_orientation;
96 | }
97 |
98 | QSplitter * SplitterGrid::get_inner(int row) {
99 | if (row < 0) {
100 | auto splitter = new QSplitter(invert(_main_orientation), this);
101 | _main_splitter->insertWidget(0, splitter);
102 | _main_splitter->setSizes(equal_sizes(_main_splitter->count()));
103 | _inner_splitters.insert(_inner_splitters.begin(), splitter);
104 | return splitter;
105 | }
106 | else if (static_cast(row) >= _inner_splitters.size()) {
107 | auto splitter = new QSplitter(invert(_main_orientation), this);
108 | _main_splitter->addWidget(splitter);
109 | _main_splitter->setSizes(equal_sizes(_main_splitter->count()));
110 | _inner_splitters.push_back(splitter);
111 | return splitter;
112 | }
113 | else {
114 | return _inner_splitters[row];
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/player/src/ui/native/osx.cpp:
--------------------------------------------------------------------------------
1 | #include "ui/native/osx.hpp"
2 |
3 | FindWindowResult find_windows_by_title_and_pname(std::string, std::string) {
4 | return { };
5 | }
6 |
7 | void toggle_window_borders(WindowHandle, bool) { }
8 |
9 | void toggle_always_on_top(WindowHandle, bool) { }
10 |
11 | void sysclose_window(WindowHandle) { }
12 |
13 | void redraw(WindowHandle) { }
14 |
15 | void set_transparent(WindowHandle) { }
16 |
--------------------------------------------------------------------------------
/player/src/ui/native/win32.cpp:
--------------------------------------------------------------------------------
1 | #include "ui/native/win32.hpp"
2 |
3 | #include
4 | #include
5 |
6 | template
7 | struct FindWindowContext {
8 | Predicate predicate;
9 | FindWindowResult matches;
10 | };
11 |
12 | template
13 | static BOOL on_window(HWND handle, LPARAM erased_context)
14 | {
15 | auto context = reinterpret_cast *>(erased_context);
16 |
17 | if (context->predicate(handle))
18 | context->matches.insert(handle);
19 |
20 | return TRUE;
21 | }
22 |
23 | template
24 | static FindWindowResult find_windows_by(Pred pred) {
25 | auto context = FindWindowContext { pred };
26 |
27 | (void)EnumWindows(on_window, reinterpret_cast(&context));
28 |
29 | return context.matches;
30 | }
31 |
32 | static std::string window_title(HWND handle) {
33 | char buff[256];
34 | auto size = GetWindowTextA(handle, buff, sizeof buff);
35 | return { buff, static_cast(size) };
36 | }
37 |
38 | static std::string window_process_name(HWND handle) {
39 | constexpr auto PROCESS_FLAGS = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ;
40 |
41 | DWORD pid;
42 | GetWindowThreadProcessId(handle, &pid);
43 |
44 | if (auto phandle = OpenProcess(PROCESS_FLAGS, FALSE, pid); phandle) {
45 | char buff[256];
46 | auto size = GetModuleFileNameExA(phandle, nullptr, buff, sizeof buff);
47 | return { buff, static_cast(size) };
48 | } else {
49 | return { };
50 | }
51 | }
52 |
53 | FindWindowResult find_windows_by_title_and_pname(std::regex title_pattern,
54 | std::string pname)
55 | {
56 | auto predicate = [&](HWND handle) {
57 | auto handle_title = window_title(handle);
58 | auto handle_pname = window_process_name(handle);
59 |
60 | auto pname_matches = handle_pname.compare(0, pname.size(), pname) == 0;
61 | auto title_matches = std::regex_search(handle_title, title_pattern);
62 |
63 | return pname_matches && title_matches;
64 | };
65 |
66 | return find_windows_by(predicate);
67 | }
68 |
69 | void toggle_window_borders(HWND handle, bool on) {
70 | constexpr auto BORDER_FLAGS = WS_CAPTION
71 | | WS_THICKFRAME
72 | | WS_MINIMIZEBOX
73 | | WS_MAXIMIZEBOX
74 | | WS_SYSMENU
75 | ;
76 |
77 | constexpr auto REPOSITION_FLAGS = SWP_DRAWFRAME
78 | | SWP_FRAMECHANGED
79 | | SWP_NOMOVE
80 | | SWP_NOSIZE
81 | | SWP_NOZORDER
82 | | SWP_NOACTIVATE
83 | ;
84 |
85 | auto current_style = GetWindowLong(handle, GWL_STYLE);
86 | auto new_style = on
87 | ? current_style | BORDER_FLAGS
88 | : current_style & ~BORDER_FLAGS;
89 | SetWindowLong(handle, GWL_STYLE, new_style);
90 | SetWindowPos(handle, nullptr, 0, 0, 0, 0, REPOSITION_FLAGS);
91 | redraw(handle);
92 | }
93 |
94 | void toggle_always_on_top(HWND handle, bool on) {
95 | auto insert_flags = on ? HWND_TOPMOST : HWND_NOTOPMOST;
96 | SetWindowPos(handle, insert_flags, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
97 | }
98 |
99 | void sysclose_window(HWND handle) {
100 | SendMessage(handle, WM_SYSCOMMAND, SC_CLOSE, 0);
101 | }
102 |
103 | void redraw(HWND handle) {
104 | InvalidateRect(handle, nullptr, TRUE);
105 | }
106 |
107 | void set_transparent(HWND handle) {
108 | auto styles = GetWindowLong(handle, GWL_EXSTYLE);
109 | SetWindowLong(handle, GWL_EXSTYLE, styles | WS_EX_TRANSPARENT);
110 | }
111 |
--------------------------------------------------------------------------------
/player/src/ui/native/x11.cpp:
--------------------------------------------------------------------------------
1 | #include "ui/native/x11.hpp"
2 |
3 | #include
4 | #include
5 |
6 | #include
7 |
8 | FindWindowResult find_windows_by_title_and_pname(std::string title, std::string pname) {
9 | auto matches = FindWindowResult { };
10 |
11 | auto display = XOpenDisplay(nullptr);
12 | auto root_window = XRootWindow(display, 0);
13 |
14 | auto open_set = std::deque { root_window };
15 |
16 | while (!open_set.empty()) {
17 | auto window = open_set.front();
18 | open_set.pop_front();
19 |
20 | Window _root, _parent;
21 | Window *children;
22 | unsigned int child_count;
23 | XQueryTree(display, window, &_root, &_parent, &children, &child_count);
24 |
25 | for (auto i = 0u; i < child_count; ++i) {
26 | auto child = children[i];
27 | XTextProperty property;
28 | XGetWMName(display, child, &property);
29 | if (property.value) {
30 | if (title.compare(reinterpret_cast(property.value)) == 0)
31 | matches.insert(child);
32 | }
33 |
34 | open_set.push_back(child);
35 | }
36 | XFree(children);
37 | }
38 |
39 | XCloseDisplay(display);
40 | return matches;
41 | }
42 |
43 | void toggle_window_borders(WindowHandle, bool) {
44 | // TODO
45 | }
46 |
47 | void toggle_always_on_top(WindowHandle, bool) {
48 | // TODO
49 | }
50 |
51 | void sysclose_window(WindowHandle) {
52 | // TODO
53 | }
54 |
55 | void redraw(WindowHandle) {
56 | // TODO
57 | }
58 |
59 | void set_transparent(WindowHandle) {
60 | // TODO
61 | }
62 |
--------------------------------------------------------------------------------
/player/src/ui/tools/about_dialog.cpp:
--------------------------------------------------------------------------------
1 | #include "ui/tools/about_dialog.hpp"
2 |
3 | #include "ui_about_dialog.h"
4 |
5 | constexpr auto VERSION = "0.9.24";
6 |
7 | AboutDialog::AboutDialog(QWidget *parent):
8 | QDialog(parent),
9 | _ui(std::make_unique())
10 | {
11 | setModal(true);
12 | _ui->setupUi(this);
13 |
14 | _ui->labelVersion->setText(QString("Twitch Player version %1").arg(VERSION));
15 | }
16 |
17 | AboutDialog::~AboutDialog() = default;
18 |
--------------------------------------------------------------------------------
/player/src/ui/tools/video_filters.cpp:
--------------------------------------------------------------------------------
1 | #include "ui/tools/video_filters.hpp"
2 | #include "ui_filters_tool.h"
3 |
4 | #include "libvlc/bindings.hpp"
5 |
6 | VideoFilters::VideoFilters(libvlc::MediaPlayer &mp, QWidget *parent):
7 | QWidget(parent),
8 | _ui(std::make_unique())
9 | {
10 | _ui->setupUi(this);
11 | setWindowFlags(Qt::Tool);
12 |
13 | auto setup_slider = [&](auto factor, auto slider, auto get, auto set) {
14 | slider->setValue((mp.*get)() * factor);
15 | auto update = [=, &mp](int value) { (mp.*set)(value / factor); };
16 | QObject::connect(slider, &QSlider::valueChanged, update);
17 | };
18 |
19 | _ui->videoFiltersGroupBox->setChecked(mp.video_filters_enabled());
20 | QObject::connect(_ui->videoFiltersGroupBox, &QGroupBox::toggled, [&](bool on) {
21 | mp.enable_video_filters(on);
22 | });
23 |
24 | using MP = libvlc::MediaPlayer;
25 | setup_slider(100.f, _ui->sliderContrast, &MP::get_contrast, &MP::set_contrast);
26 | setup_slider(100.f, _ui->sliderBrightness, &MP::get_brightness, &MP::set_brightness);
27 | setup_slider(1, _ui->sliderHue, &MP::get_hue, &MP::set_hue);
28 | setup_slider(100.f, _ui->sliderSaturation, &MP::get_saturation, &MP::set_saturation);
29 | setup_slider(100.f, _ui->sliderGamma, &MP::get_gamma, &MP::set_gamma);
30 | }
31 |
--------------------------------------------------------------------------------
/player/src/ui/tray.cpp:
--------------------------------------------------------------------------------
1 | #include "ui/tray.hpp"
2 |
3 | #include "api/pubsub.hpp"
4 |
5 | #include "prelude/timer.hpp"
6 |
7 | #include "constants.hpp"
8 |
9 | #include
10 |
11 | SystemTray::SystemTray(TwitchPubSub &pubsub):
12 | QSystemTrayIcon(QIcon(":/icons/icon.ico")),
13 | _pubsub(pubsub)
14 | {
15 | QObject::connect(&pubsub, &TwitchPubSub::channel_went_live, [=](auto channel) {
16 | channel_to_play = channel;
17 |
18 | auto alert_title = QString("%1 just went live!").arg(channel);
19 | auto alert_message = QString("Click to play");
20 |
21 | showMessage(alert_title, alert_message);
22 |
23 | delayed(this, 10'000, [=] { channel_to_play = std::nullopt; });
24 | });
25 |
26 | QObject::connect(this, &QSystemTrayIcon::messageClicked, [=] {
27 | if (channel_to_play)
28 | emit channel_open_requested(*channel_to_play);
29 | });
30 |
31 | _menu.addAction("Quit", [] { qApp->quit(); });
32 |
33 | setContextMenu(&_menu);
34 |
35 | using namespace constants::settings::notifications;
36 |
37 | QSettings settings;
38 |
39 | auto pubsub_channels = settings
40 | .value(KEY_PUBSUB_CHANNELS, DEFAULT_PUBSUB_CHANNELS)
41 | .toStringList();
42 |
43 | for (auto channel: pubsub_channels)
44 | pubsub.listen_to_channel(channel)
45 | .fail([=](QString error) {
46 | auto alert_title = "Failed to monitor channel";
47 | auto alert_message = QString("Error while trying to monitor %1: %2")
48 | .arg(channel)
49 | .arg(error);
50 |
51 | showMessage(alert_title, alert_message, QSystemTrayIcon::Critical);
52 | });
53 | }
54 |
--------------------------------------------------------------------------------
/player/src/ui/utils/event_notifier.cpp:
--------------------------------------------------------------------------------
1 | #include "ui/utils/event_notifier.hpp"
2 |
3 | EventNotifier::EventNotifier(std::initializer_list events, QObject *parent):
4 | QObject(parent),
5 | _monitored_events(events)
6 | { }
7 |
8 | bool EventNotifier::eventFilter(QObject *watched, QEvent *event) {
9 | if (_monitored_events.count(event->type()))
10 | emit new_event(*event);
11 |
12 | return QObject::eventFilter(watched, event);
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/player/src/ui/widgets/chat_pane.cpp:
--------------------------------------------------------------------------------
1 | #include "ui/widgets/chat_pane.hpp"
2 | #include "ui/widgets/foreign_widget.hpp"
3 | #include "ui/native/capabilities.hpp"
4 |
5 | #include "constants.hpp"
6 |
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 |
13 | #include
14 |
15 | constexpr auto border_width = 1;
16 |
17 | static auto find_chat_windows(QString renderer_path) {
18 | using namespace constants::settings::chat_renderer;
19 |
20 | auto normalized_path = renderer_path.replace("/", "\\").toStdString();
21 |
22 | QSettings settings;
23 | auto title_hint = settings
24 | .value(KEY_CHAT_RENDERER_TITLE_HINT,DEFAULT_CHAT_RENDERER_TITLE_HINT)
25 | .toString();
26 | auto title_hint_pattern = std::regex { title_hint.toLocal8Bit().data() };
27 |
28 | return find_windows_by_title_and_pname(title_hint_pattern, normalized_path);
29 | }
30 |
31 | static auto find_new_chat_windows(QString renderer_path,
32 | const FindWindowResult & previous_results) {
33 | auto new_results = find_chat_windows(renderer_path);
34 |
35 | FindWindowResult new_windows;
36 | std::set_difference(
37 | new_results.begin(), new_results.end(),
38 | previous_results.begin(), previous_results.end(),
39 | std::inserter(new_windows, new_windows.begin())
40 | );
41 |
42 | return new_windows;
43 | }
44 |
45 | ChatPane::ChatPane(QWidget *parent):
46 | QWidget(parent),
47 | _layout(new QHBoxLayout(this)),
48 | _chat(new ForeignWidget(this))
49 | {
50 | _layout->addWidget(_chat);
51 | setLayout(_layout);
52 |
53 | auto margins = QMargins(border_width, border_width, border_width, border_width);
54 | _layout->setContentsMargins(margins);
55 | setContentsMargins(margins);
56 |
57 | using namespace constants::settings::chat_renderer;
58 |
59 | QSettings settings;
60 |
61 | auto renderer_path = settings
62 | .value(KEY_CHAT_RENDERER_PATH, DEFAULT_CHAT_RENDERER_PATH)
63 | .toString();
64 | auto raw_renderer_args = settings
65 | .value(KEY_CHAT_RENDERER_ARGS, DEFAULT_CHAT_RENDERER_ARGS)
66 | .toStringList();
67 | QStringList renderer_args;
68 | for (auto raw_arg: raw_renderer_args)
69 | renderer_args.append(raw_arg.replace("${channel}", "none"));
70 | auto pre_launch_windows = find_chat_windows(renderer_path);
71 | bool started = QProcess::startDetached(renderer_path, renderer_args);
72 |
73 | if (!started)
74 | QMessageBox::critical(
75 | window(),
76 | "Error starting chat renderer",
77 | QString("Failed to start the chat process: %1").arg(renderer_path)
78 | );
79 | else {
80 | auto timer = new QTimer(this);
81 | QObject::connect(timer, &QTimer::timeout, [=] {
82 | auto chat_windows = find_new_chat_windows(renderer_path, pre_launch_windows);
83 |
84 | if (auto handle_it = chat_windows.begin();
85 | handle_it != chat_windows.end())
86 | {
87 | _chat->grab(*handle_it);
88 | activateWindow();
89 | timer->deleteLater();
90 | }
91 | });
92 | timer->start(250);
93 | }
94 | }
95 |
96 | ChatPane::~ChatPane() = default;
97 |
--------------------------------------------------------------------------------
/player/src/ui/widgets/foreign_widget.cpp:
--------------------------------------------------------------------------------
1 | #include "ui/widgets/foreign_widget.hpp"
2 |
3 | #include "ui/native/capabilities.hpp"
4 |
5 | #include
6 | #include
7 |
8 | ForeignWidget::ForeignWidget(QWidget *parent):
9 | QWidget(parent),
10 | _layout(new QHBoxLayout(this))
11 | {
12 | setLayout(_layout);
13 |
14 | _layout->setContentsMargins(QMargins());
15 | setContentsMargins(QMargins());
16 | }
17 |
18 | void ForeignWidget::grab(WindowHandle handle) {
19 | release_window();
20 |
21 | auto abstract_handle = from_native_handle(handle);
22 | if (auto win_ptr = QWindow::fromWinId(abstract_handle); win_ptr) {
23 | _foreign_win_ptr = win_ptr;
24 | if (auto container = QWidget::createWindowContainer(win_ptr); container) {
25 | _container = container;
26 | _layout->addWidget(container);
27 | // Forcing an initial redraw seems to help fixing reparenting issues
28 | redraw();
29 | }
30 | }
31 | }
32 |
33 | ForeignWidget::~ForeignWidget() {
34 | release_window();
35 | }
36 |
37 | void ForeignWidget::release_window() {
38 | if (_foreign_win_ptr) {
39 | auto win_ptr = *_foreign_win_ptr;
40 | auto handle = to_native_handle(win_ptr->winId());
41 |
42 | win_ptr->setParent(nullptr);
43 | sysclose_window(handle);
44 | }
45 | }
46 |
47 | void ForeignWidget::redraw() {
48 | // On Windows, there are sometimes issues where the window won't redraw
49 | // itself when the container or its parents are reparented
50 | if (_container)
51 | ::redraw(to_native_handle((*_container)->winId()));
52 | }
53 |
--------------------------------------------------------------------------------
/player/src/ui/widgets/stream_card.cpp:
--------------------------------------------------------------------------------
1 | #include "ui/widgets/stream_card.hpp"
2 | #include "ui_stream_card.h"
3 |
4 | #include
5 |
6 | #include
7 | #include
8 |
9 | StreamCard::StreamCard(StreamData data, QWidget *parent):
10 | QWidget(parent),
11 | _ui(std::make_unique()),
12 | _uptime_widget(new QWidget(nullptr)),
13 | _uptime_layout(new QHBoxLayout(_uptime_widget)),
14 | _uptime_logo(new QLabel(this)),
15 | _uptime_label(new QLabel(this)),
16 | _data(data)
17 | {
18 | _ui->setupUi(this);
19 |
20 | _ui->channelName->setText(data.channel.display_name);
21 | _ui->title->setText(data.channel.title);
22 | _ui->gameName->setText(data.current_game);
23 | _ui->viewerCount->setText(QString("%1 viewers").arg(data.viewcount));
24 |
25 | _uptime_widget->setParent(_ui->preview);
26 | _uptime_widget->setStyleSheet("background-color: rgba(0, 0, 0, 0.1);");
27 | _uptime_layout->addWidget(_uptime_logo);
28 | _uptime_layout->addWidget(_uptime_label);
29 |
30 | _uptime_logo->setPixmap(QPixmap(":/icons/clock.svg"));
31 | _uptime_logo->setScaledContents(true);
32 | _uptime_logo->setMaximumSize(16, 16);
33 |
34 | auto uptime_secs = data.created_at.secsTo(QDateTime::currentDateTime());
35 | auto uptime_hours = uptime_secs / 3600;
36 | auto uptime_minutes = (uptime_secs - uptime_hours * 3600) / 60;
37 | _uptime_label->setText(QString("%1:%2").arg(uptime_hours).arg(uptime_minutes, 2, 10, QChar('0')));
38 |
39 | QPalette palette;
40 | palette.setColor(QPalette::WindowText, Qt::white);
41 | _uptime_label->setPalette(palette);
42 |
43 | _uptime_widget->show();
44 | _uptime_widget->move(_ui->preview->width() - _uptime_widget->width(), 0);
45 |
46 | auto http_client = new QNetworkAccessManager(this);
47 | http_client->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
48 | auto preview_reply = http_client->get(QNetworkRequest(QUrl(data.preview)));
49 |
50 | QObject::connect(preview_reply, &QNetworkReply::finished, [=] {
51 | auto data = preview_reply->readAll();
52 | _ui->preview->setPixmap(QPixmap::fromImage(QImage::fromData(data)));
53 | preview_reply->deleteLater();
54 | });
55 | }
56 |
57 | StreamCard::~StreamCard() = default;
58 |
59 | void StreamCard::mousePressEvent(QMouseEvent *) {
60 | emit clicked(_data.channel.name);
61 | }
62 |
--------------------------------------------------------------------------------
/player/src/ui/widgets/stream_pane.cpp:
--------------------------------------------------------------------------------
1 | #include "ui/widgets/stream_pane.hpp"
2 |
3 | #include "ui/widgets/stream_picker.hpp"
4 | #include "ui/widgets/stream_widget.hpp"
5 | #include "ui/widgets/video_widget.hpp"
6 | #include "ui/overlays/video_controls.hpp"
7 | #include "ui/utils/event_notifier.hpp"
8 |
9 | #include "libvlc/bindings.hpp"
10 |
11 | #include "prelude/timer.hpp"
12 |
13 | #include
14 | #include
15 | #include
16 |
17 | constexpr auto border_width = 1;
18 |
19 | static const std::initializer_list FOCUS_INVALIDATING_EVENTS = {
20 | QEvent::MouseButtonRelease,
21 | QEvent::KeyRelease
22 | };
23 |
24 | StreamPane::StreamPane(libvlc::Instance &video_ctx, QWidget *parent):
25 | QWidget(parent),
26 | _video_ctx(video_ctx),
27 | _layout(new QHBoxLayout(this)),
28 | _picker(std::make_unique(this)),
29 | _stream(std::make_unique(video_ctx, this))
30 | {
31 | auto notifier = new EventNotifier(FOCUS_INVALIDATING_EVENTS, this);
32 | window()->installEventFilter(notifier);
33 |
34 | QObject::connect(notifier, &EventNotifier::new_event, [=] {
35 | repaint();
36 | });
37 |
38 | setup_picker();
39 | setup_stream();
40 |
41 | _stream->hide();
42 | setLayout(_layout);
43 |
44 | _layout->addWidget(_picker.get());
45 |
46 | setFocusPolicy(Qt::StrongFocus);
47 | auto margins = QMargins(border_width, border_width, border_width, border_width);
48 | _layout->setContentsMargins(margins);
49 | setContentsMargins(margins);
50 | setAutoFillBackground(true);
51 | }
52 |
53 | StreamPane::~StreamPane() = default;
54 |
55 | void StreamPane::play(QString channel, QString quality) {
56 | _picker->hide();
57 |
58 | _layout->removeWidget(_picker.get());
59 | _layout->addWidget(_stream.get());
60 |
61 | _stream->setFocus();
62 | _stream->show();
63 | _stream->play(channel, quality);
64 |
65 | repaint();
66 | }
67 |
68 | StreamWidget *StreamPane::stream() const {
69 | return _stream.get();
70 | }
71 |
72 | void StreamPane::paintEvent(QPaintEvent *event) {
73 | if (isAncestorOf(qApp->focusWidget())) {
74 | QPainter painter(this);
75 |
76 | painter.setPen({
77 | QColor(0x39, 0x2e, 0x5c),
78 | static_cast(border_width),
79 | Qt::PenStyle::DotLine
80 | });
81 | painter.drawRect(
82 | border_width, border_width,
83 | width() - border_width * 2,
84 | height() - border_width * 2
85 | );
86 | }
87 |
88 | QWidget::paintEvent(event);
89 | }
90 |
91 | void StreamPane::focusOutEvent(QFocusEvent *event) {
92 | QWidget::focusOutEvent(event);
93 | repaint();
94 | }
95 |
96 | void StreamPane::focusInEvent(QFocusEvent *event) {
97 | QWidget::focusInEvent(event);
98 | if (auto item = _layout->itemAt(0); item)
99 | item->widget()->setFocus();
100 | repaint();
101 | }
102 |
103 | void StreamPane::setup_picker() {
104 | auto play_stream = [=](QString channel, QString quality) {
105 | play(channel, quality);
106 | };
107 |
108 | QObject::connect( _picker.get(), &StreamPicker::stream_picked, play_stream);
109 | }
110 |
111 | void StreamPane::setup_stream() {
112 | auto on_browse = [=] {
113 | // For some unknown reasons, the foreign widget doesn't get properly
114 | // released unless we schedule the removing for later...
115 | delayed(this, 250, [=] {
116 | _layout->removeWidget(_stream.get());
117 |
118 | _picker = std::make_unique(this);
119 | _stream = std::make_unique(_video_ctx, this);
120 | setup_picker();
121 | setup_stream();
122 |
123 | _layout->addWidget(_picker.get());
124 |
125 | _picker->show();
126 | });
127 | };
128 |
129 | QObject::connect(
130 | &_stream->video()->controls(),
131 | &VideoControls::browse_requested,
132 | on_browse
133 | );
134 |
135 | QObject::connect(
136 | &_stream->video()->controls(),
137 | &VideoControls::remove_requested,
138 | [=] { emit remove_requested(); }
139 | );
140 | QObject::connect(
141 | &_stream->video()->controls(),
142 | &VideoControls::zoom_requested,
143 | [=](bool on) {
144 | emit zoom_requested(on);
145 | _stream->video()->controls().set_zoomed(on);
146 | }
147 | );
148 | QObject::connect(
149 | &_stream->video()->controls(),
150 | &VideoControls::fullscreen_requested,
151 | [=](bool on) {
152 | emit fullscreen_requested(on);
153 | _stream->video()->controls().set_fullscreen(on);
154 | }
155 | );
156 | }
157 |
--------------------------------------------------------------------------------
/player/src/ui/widgets/stream_picker.cpp:
--------------------------------------------------------------------------------
1 | #include "ui/widgets/stream_picker.hpp"
2 | #include "ui_stream_picker.h"
3 |
4 | #include "ui/layouts/flow.hpp"
5 | #include "ui/widgets/stream_card.hpp"
6 |
7 | #include "api/oauth.hpp"
8 |
9 | #include "constants.hpp"
10 |
11 | #include
12 |
13 | StreamPicker::StreamPicker(QWidget *parent):
14 | QWidget(parent),
15 | _ui(std::make_unique()),
16 | _channels_stream_presenter(new QWidget(this)),
17 | _followed_stream_presenter(new QWidget(this))
18 | {
19 | _ui->setupUi(this);
20 |
21 | new FlowLayout(_channels_stream_presenter);
22 | _ui->channelsStreamArea->setWidget(_channels_stream_presenter);
23 | new FlowLayout(_followed_stream_presenter);
24 | _ui->followedStreamArea->setWidget(_followed_stream_presenter);
25 |
26 | QSettings settings;
27 |
28 | auto access_token = settings
29 | .value(constants::settings::oauth::ACCESS_TOKEN_KEY)
30 | .toString();
31 |
32 | fetch_streams(_api.top_streams(), _channels_stream_presenter);
33 |
34 | if (!access_token.isEmpty())
35 | fetch_streams(_api.followed_streams(access_token), _followed_stream_presenter);
36 |
37 | QObject::connect(_ui->searchBox, &QLineEdit::textChanged, [this](auto query) {
38 | if (query.isEmpty())
39 | fetch_streams(_api.top_streams(), _channels_stream_presenter);
40 | else
41 | fetch_streams(_api.stream_search(query), _channels_stream_presenter);
42 | });
43 |
44 | QObject::connect(_ui->searchBox, &QLineEdit::returnPressed, [this] {
45 | channel_picked(_ui->searchBox->text());
46 | });
47 | }
48 |
49 | StreamPicker::~StreamPicker() = default;
50 |
51 | void StreamPicker::focusInEvent(QFocusEvent *event) {
52 | QWidget::focusInEvent(event);
53 | _ui->searchBox->setFocus();
54 | }
55 |
56 | void StreamPicker::channel_picked(QString channel) {
57 | using namespace constants::settings::streams;
58 |
59 | QSettings settings;
60 | auto quality = settings
61 | .value(KEY_LAST_QUALITY_FOR(channel))
62 | .toString();
63 |
64 | emit stream_picked(channel, quality);
65 | }
66 |
67 | void StreamPicker::fetch_streams(TwitchAPI::streams_response_t query, QWidget *container) {
68 | auto & current_query = current_queries[container];
69 |
70 | if (current_query)
71 | current_query->cancel();
72 |
73 | auto [token, cquery] = make_cancellable(query);
74 |
75 | auto present_streams_on_container = [=](QList stream_data) {
76 | present_streams(container, stream_data);
77 | };
78 |
79 | cquery.then(present_streams_on_container);
80 |
81 | current_query = token;
82 | }
83 |
84 | void StreamPicker::present_streams(QWidget *container, QList stream_data) {
85 | auto layout = container->layout();
86 |
87 | QLayoutItem *item;
88 | while ((item = layout->takeAt(0)) != nullptr) {
89 | item->widget()->deleteLater();
90 | delete item;
91 | }
92 |
93 | for (auto data: stream_data) {
94 | auto stream_card = new StreamCard(data, container);
95 | layout->addWidget(stream_card);
96 | QObject::connect(stream_card, &StreamCard::clicked, [this](auto channel) {
97 | channel_picked(channel);
98 | });
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/player/vendor/qtpromise-0.3.0/.gitignore:
--------------------------------------------------------------------------------
1 | _book
2 | dist
3 | node_modules
4 | *.gcno
5 | *.gcda
6 | *.moc
7 | *.o
8 | *.obj
9 | *.exe
10 | *.user
11 | Makefile*
12 | moc_*.cpp
13 | moc_*.h
14 | coverage.info
15 | package-lock.json
16 | target_wrapper.bat
17 |
--------------------------------------------------------------------------------
/player/vendor/qtpromise-0.3.0/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 | dist: trusty
3 | language: cpp
4 | compiler: gcc
5 |
6 | before_install:
7 | - sudo add-apt-repository -y ppa:beineri/opt-qt542-trusty
8 | - sudo apt-get update -qq
9 |
10 | install:
11 | - sudo apt-get install -qq qt54base
12 | - source /opt/qt54/bin/qt54-env.sh
13 | - wget http://archive.ubuntu.com/ubuntu/pool/universe/l/lcov/lcov_1.13.orig.tar.gz
14 | - tar xf lcov_1.13.orig.tar.gz
15 | - cd lcov-1.13/
16 | - sudo make install
17 | - cd ..
18 |
19 | before_script:
20 | - qmake --version
21 | - lcov --version
22 | - gcc --version
23 |
24 | script:
25 | - qmake qtpromise.pro CONFIG+=coverage
26 | - make -j4
27 | - make check --quiet
28 | - lcov -capture --directory . --o coverage.info
29 | - lcov -e coverage.info '**/src/**/*' -o coverage.info
30 |
31 | after_success:
32 | - bash <(curl -s https://codecov.io/bash) -f coverage.info
33 |
34 |
35 | # gitbook install .
36 | # gitbook build . dist/docs
37 |
--------------------------------------------------------------------------------
/player/vendor/qtpromise-0.3.0/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Simon Brunel
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/player/vendor/qtpromise-0.3.0/README.md:
--------------------------------------------------------------------------------
1 |