vlcsync 0.2.1__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {vlcsync-0.2.1 → vlcsync-0.3.1}/PKG-INFO +8 -4
- {vlcsync-0.2.1 → vlcsync-0.3.1}/README.md +7 -3
- {vlcsync-0.2.1 → vlcsync-0.3.1}/pyproject.toml +1 -1
- vlcsync-0.3.1/setup.py +34 -0
- {vlcsync-0.2.1 → vlcsync-0.3.1}/vlcsync/cli.py +19 -6
- vlcsync-0.3.1/vlcsync/syncer.py +114 -0
- vlcsync-0.3.1/vlcsync/vlc.py +246 -0
- {vlcsync-0.2.1 → vlcsync-0.3.1}/vlcsync/vlc_finder.py +6 -5
- {vlcsync-0.2.1 → vlcsync-0.3.1}/vlcsync/vlc_state.py +36 -11
- vlcsync-0.2.1/setup.py +0 -34
- vlcsync-0.2.1/vlcsync/syncer.py +0 -84
- vlcsync-0.2.1/vlcsync/vlc.py +0 -176
- {vlcsync-0.2.1 → vlcsync-0.3.1}/LICENSE.md +0 -0
- {vlcsync-0.2.1 → vlcsync-0.3.1}/vlcsync/__init__.py +0 -0
- {vlcsync-0.2.1 → vlcsync-0.3.1}/vlcsync/cli_utils.py +0 -0
- {vlcsync-0.2.1 → vlcsync-0.3.1}/vlcsync/main.py +0 -0
- {vlcsync-0.2.1 → vlcsync-0.3.1}/vlcsync/vlc_socket.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: vlcsync
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.1
|
4
4
|
Summary: Utility for synchronize multiple instances of VLC. Supports seek, play and pause.
|
5
5
|
Home-page: https://github.com/mrkeuz/vlcsync/
|
6
6
|
License: MIT
|
@@ -29,7 +29,7 @@ Description-Content-Type: text/markdown
|
|
29
29
|
VLC Sync
|
30
30
|
========
|
31
31
|
|
32
|
-
Utility for synchronize multiple instances of VLC. Supports seek, play and pause.
|
32
|
+
Utility for synchronize multiple instances of VLC. Supports seek, play and pause/stop, playlist and volume sync.
|
33
33
|
|
34
34
|
|
35
35
|
#### Motivation
|
@@ -71,6 +71,10 @@ $ vlcsync --rc-host 192.168.1.100:12345 --rc-host 192.168.1.50:54321
|
|
71
71
|
# For disable local discovery (only remote instances)
|
72
72
|
$ vlcsync --no-local-discovery --rc-host 192.168.1.100:12345
|
73
73
|
|
74
|
+
# Started from version 0.3.0 (playlists sync)
|
75
|
+
# Support volume sync for exotic cases
|
76
|
+
$ vlcsync --volume-sync
|
77
|
+
|
74
78
|
# For help and see all options
|
75
79
|
$ vlcsync --help
|
76
80
|
```
|
@@ -89,7 +93,7 @@ Awesome [use-case](./docs/awesome.md) ideas
|
|
89
93
|
Difference between videos can be **up to ~0.5 seconds** in worst case. Especially when playing from network share,
|
90
94
|
due buffering time and network latency.
|
91
95
|
|
92
|
-
- Currently, tested
|
96
|
+
- Currently, tested on:
|
93
97
|
- Linux (Ubuntu 20.04)
|
94
98
|
- Windows 7 (32-bit)
|
95
99
|
- Windows 10 (64-bit)
|
@@ -102,7 +106,7 @@ Awesome [use-case](./docs/awesome.md) ideas
|
|
102
106
|
- [Syncplay](https://github.com/Syncplay/syncplay) - very promised, but little [complicated](https://github.com/Syncplay/syncplay/discussions/463) for sync different videos
|
103
107
|
- [bino](https://bino3d.org/) - working, very strange controls, file dialog not working and only fullscreen
|
104
108
|
- [gridplayer](https://github.com/vzhd1701/gridplayer) - low fps by some reason
|
105
|
-
- [mpv](https://github.com/mpv-player/mpv) - with [mixing multiple videos](https://superuser.com/a/1325668/1272472) in one window.
|
109
|
+
- [mpv](https://github.com/mpv-player/mpv) - with [mixing multiple videos](https://superuser.com/a/1325668/1272472) in one window. Unfortunately does not support multiple screens
|
106
110
|
- [AVPlayer](http://www.awesomevideoplayer.com/) - only Win, macOS, up to 4 videos in free version
|
107
111
|
|
108
112
|
## Contributing
|
@@ -1,7 +1,7 @@
|
|
1
1
|
VLC Sync
|
2
2
|
========
|
3
3
|
|
4
|
-
Utility for synchronize multiple instances of VLC. Supports seek, play and pause.
|
4
|
+
Utility for synchronize multiple instances of VLC. Supports seek, play and pause/stop, playlist and volume sync.
|
5
5
|
|
6
6
|
|
7
7
|
#### Motivation
|
@@ -43,6 +43,10 @@ $ vlcsync --rc-host 192.168.1.100:12345 --rc-host 192.168.1.50:54321
|
|
43
43
|
# For disable local discovery (only remote instances)
|
44
44
|
$ vlcsync --no-local-discovery --rc-host 192.168.1.100:12345
|
45
45
|
|
46
|
+
# Started from version 0.3.0 (playlists sync)
|
47
|
+
# Support volume sync for exotic cases
|
48
|
+
$ vlcsync --volume-sync
|
49
|
+
|
46
50
|
# For help and see all options
|
47
51
|
$ vlcsync --help
|
48
52
|
```
|
@@ -61,7 +65,7 @@ Awesome [use-case](./docs/awesome.md) ideas
|
|
61
65
|
Difference between videos can be **up to ~0.5 seconds** in worst case. Especially when playing from network share,
|
62
66
|
due buffering time and network latency.
|
63
67
|
|
64
|
-
- Currently, tested
|
68
|
+
- Currently, tested on:
|
65
69
|
- Linux (Ubuntu 20.04)
|
66
70
|
- Windows 7 (32-bit)
|
67
71
|
- Windows 10 (64-bit)
|
@@ -74,7 +78,7 @@ Awesome [use-case](./docs/awesome.md) ideas
|
|
74
78
|
- [Syncplay](https://github.com/Syncplay/syncplay) - very promised, but little [complicated](https://github.com/Syncplay/syncplay/discussions/463) for sync different videos
|
75
79
|
- [bino](https://bino3d.org/) - working, very strange controls, file dialog not working and only fullscreen
|
76
80
|
- [gridplayer](https://github.com/vzhd1701/gridplayer) - low fps by some reason
|
77
|
-
- [mpv](https://github.com/mpv-player/mpv) - with [mixing multiple videos](https://superuser.com/a/1325668/1272472) in one window.
|
81
|
+
- [mpv](https://github.com/mpv-player/mpv) - with [mixing multiple videos](https://superuser.com/a/1325668/1272472) in one window. Unfortunately does not support multiple screens
|
78
82
|
- [AVPlayer](http://www.awesomevideoplayer.com/) - only Win, macOS, up to 4 videos in free version
|
79
83
|
|
80
84
|
## Contributing
|
vlcsync-0.3.1/setup.py
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
from setuptools import setup
|
3
|
+
|
4
|
+
packages = \
|
5
|
+
['vlcsync']
|
6
|
+
|
7
|
+
package_data = \
|
8
|
+
{'': ['*']}
|
9
|
+
|
10
|
+
install_requires = \
|
11
|
+
['cached-property', 'click>=8.1.3,<9.0.0', 'loguru', 'psutil']
|
12
|
+
|
13
|
+
entry_points = \
|
14
|
+
{'console_scripts': ['vlcsync = vlcsync.main:main']}
|
15
|
+
|
16
|
+
setup_kwargs = {
|
17
|
+
'name': 'vlcsync',
|
18
|
+
'version': '0.3.1',
|
19
|
+
'description': 'Utility for synchronize multiple instances of VLC. Supports seek, play and pause. ',
|
20
|
+
'long_description': 'VLC Sync\n========\n\nUtility for synchronize multiple instances of VLC. Supports seek, play and pause/stop, playlist and volume sync. \n \n\n#### Motivation\n\nStrongly inspired by F1 streams with extra driver tracking data streams. Did [not find](#alternatives) reasonable alternative for Linux for playing several videos synchronously. So decided to write my own solution.\n\n## Install\n\n```shell\npip3 install -U vlcsync\n```\n\nor \n\n- Download [binary release](https://github.com/mrkeuz/vlcsync/releases) for Windows 7/10 \n NOTE: On some systems there are false positive Antivirus warnings [issues](https://github.com/mrkeuz/vlcsync/issues/1).\n In this case use [alternative way](./docs/install.md#windows-detailed-instructions) to install. \n\n## Run\n\n`Vlc` players should open with `--rc-host 127.0.0.42` option OR configured properly from gui (see [how configure vlc](./docs/vlc_setup.md)) \n\n```shell\n\n# Run vlc players \n$ vlc --rc-host 127.0.0.42 SomeMedia1.mkv &\n$ vlc --rc-host 127.0.0.42 SomeMedia2.mkv &\n$ vlc --rc-host 127.0.0.42 SomeMedia3.mkv &\n\n# vlcsync will monitor and syncing all players\n$ vlcsync\n\n# Started from version 0.2.0\n\n# For control remote vlc instances, \n# remote port should be open and rc interface listen on 0.0.0.0\n$ vlcsync --rc-host 192.168.1.100:12345 --rc-host 192.168.1.50:54321\n\n# For disable local discovery (only remote instances)\n$ vlcsync --no-local-discovery --rc-host 192.168.1.100:12345\n\n# Started from version 0.3.0 (playlists sync)\n# Support volume sync for exotic cases\n$ vlcsync --volume-sync\n\n# For help and see all options\n$ vlcsync --help\n```\n\n## Awesome \n\nAwesome [use-case](./docs/awesome.md) ideas\n\n## Demo\n\n\n\n## Limitations \n\n- Frame-to-frame sync NOT provided. `vlc` does not have precise controlling via `rc` interface out of box. \n Difference between videos can be **up to ~0.5 seconds** in worst case. Especially when playing from network share, \n due buffering time and network latency.\n\n- Currently, tested on:\n - Linux (Ubuntu 20.04)\n - Windows 7 (32-bit)\n - Windows 10 (64-bit)\n\n## Alternatives\n\n- [vlc](https://www.videolan.org/vlc/index.ru.html) \n - There is a [netsync](https://wiki.videolan.org/Documentation:Modules/netsync/) but seem only master-slave (tried, but not working by some reason)\n - Open additional media. Seems feature broken in vlc 3 (also afaik limited only 2 streams) \n- [Syncplay](https://github.com/Syncplay/syncplay) - very promised, but little [complicated](https://github.com/Syncplay/syncplay/discussions/463) for sync different videos\n- [bino](https://bino3d.org/) - working, very strange controls, file dialog not working and only fullscreen\n- [gridplayer](https://github.com/vzhd1701/gridplayer) - low fps by some reason\n- [mpv](https://github.com/mpv-player/mpv) - with [mixing multiple videos](https://superuser.com/a/1325668/1272472) in one window. Unfortunately does not support multiple screens\n- [AVPlayer](http://www.awesomevideoplayer.com/) - only Win, macOS, up to 4 videos in free version\n\n## Contributing\n\nAny thoughts, ideas and contributions welcome!\n\nA special thanks to **KorDen32** for inspiration! <img src="./docs/F1.svg" alt="F1" width="45"/>\n\nEnjoy!\n',
|
21
|
+
'author': 'mrkeuz',
|
22
|
+
'author_email': 'mrkeuz@users.noreply.github.com',
|
23
|
+
'maintainer': None,
|
24
|
+
'maintainer_email': None,
|
25
|
+
'url': 'https://github.com/mrkeuz/vlcsync/',
|
26
|
+
'packages': packages,
|
27
|
+
'package_data': package_data,
|
28
|
+
'install_requires': install_requires,
|
29
|
+
'entry_points': entry_points,
|
30
|
+
'python_requires': '>=3.8,<4.0',
|
31
|
+
}
|
32
|
+
|
33
|
+
|
34
|
+
setup(**setup_kwargs)
|
@@ -11,7 +11,8 @@ from vlcsync.syncer import AppConfig, Syncer
|
|
11
11
|
from vlcsync.vlc_finder import print_exc
|
12
12
|
from vlcsync.vlc_state import VlcId
|
13
13
|
|
14
|
-
|
14
|
+
# Also ref in project.toml
|
15
|
+
__version__ = "0.3.1"
|
15
16
|
|
16
17
|
|
17
18
|
@click.command
|
@@ -28,20 +29,32 @@ __version__ = "0.2.1"
|
|
28
29
|
required=False,
|
29
30
|
is_flag=True,
|
30
31
|
help="Disable discovery local vlc instances.")
|
31
|
-
|
32
|
+
@click.option("--volume-sync",
|
33
|
+
"volume_sync",
|
34
|
+
default=False,
|
35
|
+
required=False,
|
36
|
+
is_flag=True,
|
37
|
+
help=
|
38
|
+
"""
|
39
|
+
Enable volume sync between players. Useful for play video with external audio file.\n
|
40
|
+
I.e. first play video with disabled audio track ('vlc --no-audio video.mkv' option) and then play audio stream (`vlc audio.mka`).
|
41
|
+
\n
|
42
|
+
And you can control volume from main video player.
|
43
|
+
""")
|
44
|
+
def main(rc_host_list: Set[VlcId], no_local_discover, volume_sync):
|
32
45
|
"""Utility for synchronize multiple instances of VLC. Supports seek, play and pause."""
|
33
|
-
print("Vlcsync started...")
|
46
|
+
print("Vlcsync started...", flush=True)
|
34
47
|
|
35
|
-
app_config = AppConfig(rc_host_list, no_local_discover)
|
48
|
+
app_config = AppConfig(rc_host_list, no_local_discover, volume_sync)
|
36
49
|
time.sleep(2) # Wait instances
|
37
50
|
while True:
|
38
51
|
try:
|
39
52
|
with Syncer(app_config) as s:
|
40
53
|
while True:
|
41
|
-
s.
|
54
|
+
s.do_check_synchronized()
|
42
55
|
time.sleep(0.05)
|
43
56
|
except KeyboardInterrupt:
|
44
57
|
sys.exit(0)
|
45
58
|
except Exception:
|
46
59
|
print_exc()
|
47
|
-
print("Exception detected. Restart sync...")
|
60
|
+
print("Exception detected. Restart sync...", flush=True)
|
@@ -0,0 +1,114 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
import sys
|
5
|
+
import time
|
6
|
+
from typing import Set, List
|
7
|
+
|
8
|
+
from loguru import logger
|
9
|
+
|
10
|
+
from vlcsync.vlc import VLC_IFACE_IP, VlcProcs, Vlc
|
11
|
+
from vlcsync.vlc_finder import LocalProcessFinderProvider, ExtraHostFinder
|
12
|
+
from vlcsync.vlc_socket import VlcConnectionError
|
13
|
+
|
14
|
+
from vlcsync.vlc_state import VlcId
|
15
|
+
|
16
|
+
|
17
|
+
@dataclass
|
18
|
+
class AppConfig:
|
19
|
+
extra_rc_hosts: Set[VlcId]
|
20
|
+
no_local_discovery: bool
|
21
|
+
volume_sync: bool
|
22
|
+
|
23
|
+
|
24
|
+
class Syncer:
|
25
|
+
def __init__(self, app_config: AppConfig):
|
26
|
+
self.env = None
|
27
|
+
self.app_config = app_config
|
28
|
+
self.supress_log_until = 0
|
29
|
+
|
30
|
+
vlc_finders = set()
|
31
|
+
if not self.app_config.no_local_discovery:
|
32
|
+
vlc_finders.add(LocalProcessFinderProvider(VLC_IFACE_IP))
|
33
|
+
print(f" Discover instances on {VLC_IFACE_IP} iface...", flush=True)
|
34
|
+
else:
|
35
|
+
print(" Local discovery vlc instances DISABLED...", flush=True)
|
36
|
+
|
37
|
+
if app_config.extra_rc_hosts:
|
38
|
+
vlc_finders.add(ExtraHostFinder(app_config.extra_rc_hosts))
|
39
|
+
for rc_host in app_config.extra_rc_hosts:
|
40
|
+
rc_host: VlcId
|
41
|
+
print(f" Manual host defined {rc_host.addr}:{rc_host.port}", flush=True)
|
42
|
+
else:
|
43
|
+
print(""" Manual vlc addresses ("--rc-host" args) NOT provided...""", flush=True)
|
44
|
+
|
45
|
+
if not vlc_finders:
|
46
|
+
print("\nTarget vlc instances not selected (nor autodiscover, nor manually). \n"
|
47
|
+
"""See: "vlcsync --help" for more info""", flush=True)
|
48
|
+
sys.exit(1)
|
49
|
+
|
50
|
+
self.env = VlcProcs(vlc_finders)
|
51
|
+
|
52
|
+
def __enter__(self):
|
53
|
+
self.do_check_synchronized()
|
54
|
+
return self
|
55
|
+
|
56
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
57
|
+
self.close()
|
58
|
+
|
59
|
+
def do_check_synchronized(self):
|
60
|
+
self.log_with_debounce("do_check_synchronized()...")
|
61
|
+
try:
|
62
|
+
if self.app_config.volume_sync:
|
63
|
+
self.sync_volume()
|
64
|
+
|
65
|
+
self.sync_playstate()
|
66
|
+
|
67
|
+
except VlcConnectionError as e:
|
68
|
+
self.env.dereg(e.vlc_id)
|
69
|
+
|
70
|
+
def sync_playstate(self):
|
71
|
+
for vlc_id, vlc in self.env.all_vlc.items():
|
72
|
+
is_changed, state, playlist_changed = vlc.is_state_change()
|
73
|
+
|
74
|
+
if is_changed:
|
75
|
+
# Workaround Save volumes.
|
76
|
+
# When playlist items changed ALSO happen volumes sync by some reason. But SHOULD NOT!
|
77
|
+
volumes: List[tuple[Vlc, int]] = []
|
78
|
+
if not self.app_config.volume_sync and playlist_changed:
|
79
|
+
volumes = [(vlc1, vlc1.volume()) for _, vlc1 in self.env.all_vlc.items()]
|
80
|
+
#
|
81
|
+
|
82
|
+
print(f"\nVlc state change detected from ({vlc_id})", flush=True)
|
83
|
+
self.env.sync_all(state, vlc)
|
84
|
+
|
85
|
+
# Restore volumes if needed
|
86
|
+
if volumes:
|
87
|
+
for vlc_next, volume in volumes:
|
88
|
+
vlc_next.set_volume(volume)
|
89
|
+
# Trying to fix endless resyncing hang
|
90
|
+
# Cannot find reproduce steps for that
|
91
|
+
time.sleep(0.5)
|
92
|
+
break
|
93
|
+
|
94
|
+
def sync_volume(self):
|
95
|
+
for vlc_id, vlc in self.env.all_vlc.items():
|
96
|
+
cur_volume = vlc.volume()
|
97
|
+
if vlc.prev_volume != cur_volume:
|
98
|
+
for vlc_id_for_sync, vlc_for_sync in self.env.all_vlc.items():
|
99
|
+
vlc_for_sync.prev_volume = cur_volume
|
100
|
+
if vlc_for_sync != vlc:
|
101
|
+
vlc_for_sync.set_volume(cur_volume)
|
102
|
+
break
|
103
|
+
|
104
|
+
def log_with_debounce(self, msg: str, _debounce=5):
|
105
|
+
if time.time() > self.supress_log_until:
|
106
|
+
logger.debug(msg)
|
107
|
+
self.supress_log_until = time.time() + _debounce
|
108
|
+
|
109
|
+
def __del__(self):
|
110
|
+
self.close()
|
111
|
+
|
112
|
+
def close(self):
|
113
|
+
if self.env:
|
114
|
+
self.env.close()
|
@@ -0,0 +1,246 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from functools import lru_cache
|
4
|
+
import re
|
5
|
+
import socket
|
6
|
+
import threading
|
7
|
+
import time
|
8
|
+
from typing import Set, List, Optional
|
9
|
+
|
10
|
+
from loguru import logger
|
11
|
+
|
12
|
+
from vlcsync.vlc_finder import IVlcListFinder
|
13
|
+
from vlcsync.vlc_socket import VlcSocket
|
14
|
+
from vlcsync.vlc_state import PlayState, State, VlcId, PlayList, PlayListItem
|
15
|
+
|
16
|
+
VLC_IFACE_IP = "127.0.0.42"
|
17
|
+
RE_PLAYSTATE_COMPILED = re.compile(r"\( state (playing|stopped|paused) \)")
|
18
|
+
RE_PLAYLIST_ITEM = re.compile(r'\| {2}([ *])(\d+) - ')
|
19
|
+
|
20
|
+
socket.setdefaulttimeout(0.5)
|
21
|
+
|
22
|
+
|
23
|
+
class Vlc:
|
24
|
+
def __init__(self, vlc_id: VlcId):
|
25
|
+
self.vlc_id = vlc_id
|
26
|
+
self.vlc_conn = VlcSocket(vlc_id)
|
27
|
+
self.prev_state: State = self.cur_state()
|
28
|
+
self.prev_volume = self.volume()
|
29
|
+
|
30
|
+
def play_state(self) -> PlayState:
|
31
|
+
status = self.vlc_conn.cmd("status")
|
32
|
+
return self._extract_state(status)
|
33
|
+
|
34
|
+
def get_seek(self) -> int | None:
|
35
|
+
seek = self.vlc_conn.cmd("get_time")
|
36
|
+
if seek != '':
|
37
|
+
return int(seek)
|
38
|
+
|
39
|
+
def playlist_goto(self, vlc_internal_index: int):
|
40
|
+
self.vlc_conn.cmd(f"goto {vlc_internal_index}")
|
41
|
+
|
42
|
+
def playlist(self) -> PlayList:
|
43
|
+
cmd_resp = self.vlc_conn.cmd("playlist")
|
44
|
+
return self._extract_playlist(cmd_resp)
|
45
|
+
|
46
|
+
def volume(self) -> Optional[int]:
|
47
|
+
vol = self.vlc_conn.cmd("volume")
|
48
|
+
if vol.strip() != '':
|
49
|
+
return int(float(vol.replace(",",'.')))
|
50
|
+
else:
|
51
|
+
return None
|
52
|
+
|
53
|
+
def set_volume(self, volume: int):
|
54
|
+
self.vlc_conn.cmd(f"volume {volume}")
|
55
|
+
self.prev_volume = volume
|
56
|
+
|
57
|
+
def seek(self, seek: int):
|
58
|
+
self.vlc_conn.cmd(f"seek {seek}")
|
59
|
+
|
60
|
+
def stop(self):
|
61
|
+
self.vlc_conn.cmd("stop")
|
62
|
+
|
63
|
+
def pause(self):
|
64
|
+
self.vlc_conn.cmd("pause")
|
65
|
+
|
66
|
+
def play(self):
|
67
|
+
self.vlc_conn.cmd("play")
|
68
|
+
|
69
|
+
def cur_state(self) -> State:
|
70
|
+
cur_seek = self.get_seek()
|
71
|
+
|
72
|
+
return State(self.play_state(),
|
73
|
+
cur_seek,
|
74
|
+
self.playlist().active_order_index(),
|
75
|
+
# Abs time of video start
|
76
|
+
time.time() - (cur_seek or 0)
|
77
|
+
)
|
78
|
+
|
79
|
+
def is_state_change(self) -> (bool, State):
|
80
|
+
cur_state: State = self.cur_state()
|
81
|
+
prev_state: State = self.prev_state
|
82
|
+
full_same, playlist_same = cur_state.same(prev_state)
|
83
|
+
|
84
|
+
# Return cur_state for reduce further socket communications
|
85
|
+
return not full_same, cur_state, not playlist_same
|
86
|
+
|
87
|
+
def sync_to(self, new_state: State, source: Vlc):
|
88
|
+
|
89
|
+
self._sync_playlist(new_state)
|
90
|
+
self._sync_playstate(new_state)
|
91
|
+
self._sync_timeline(new_state, source)
|
92
|
+
|
93
|
+
self.prev_state = self.cur_state()
|
94
|
+
|
95
|
+
def _sync_timeline(self, new_state: State, source: Vlc):
|
96
|
+
cur_play_state = self.play_state()
|
97
|
+
|
98
|
+
if cur_play_state == PlayState.PAUSED and source == self:
|
99
|
+
"""
|
100
|
+
Skip sync seek with himself on pause (avoid flickering)
|
101
|
+
As half-seconds not supported and cannot to set.
|
102
|
+
"""
|
103
|
+
pass
|
104
|
+
else:
|
105
|
+
# In all other cases
|
106
|
+
self.seek(new_state.seek)
|
107
|
+
|
108
|
+
def _sync_playstate(self, new_state: State):
|
109
|
+
cur_play_state: PlayState = self.play_state()
|
110
|
+
if cur_play_state != new_state.play_state:
|
111
|
+
if new_state.play_state == PlayState.STOPPED:
|
112
|
+
self.stop()
|
113
|
+
elif new_state.play_state == PlayState.PLAYING:
|
114
|
+
if cur_play_state in [PlayState.PAUSED, PlayState.STOPPED]:
|
115
|
+
self.play()
|
116
|
+
elif new_state.play_state == PlayState.PAUSED:
|
117
|
+
if cur_play_state == PlayState.PLAYING:
|
118
|
+
self.pause()
|
119
|
+
else:
|
120
|
+
logger.warning(f"Unknown new play state {new_state.play_state} for player")
|
121
|
+
|
122
|
+
def _sync_playlist(self, new_state: State):
|
123
|
+
cur_playlist = self.playlist()
|
124
|
+
if new_state.is_play_or_pause() and cur_playlist.active_order_index() != new_state.playlist_order_idx:
|
125
|
+
if new_state.playlist_order_idx is not None and len(cur_playlist.items) > new_state.playlist_order_idx:
|
126
|
+
self.playlist_goto(cur_playlist.items[new_state.playlist_order_idx].vlc_internal_index)
|
127
|
+
else:
|
128
|
+
self.stop()
|
129
|
+
|
130
|
+
@staticmethod
|
131
|
+
@lru_cache(maxsize=128)
|
132
|
+
def _extract_state(status: str):
|
133
|
+
match = RE_PLAYSTATE_COMPILED.search(status)
|
134
|
+
return PlayState(match.group(1)) if match else PlayState.UNKNOWN
|
135
|
+
|
136
|
+
@staticmethod
|
137
|
+
@lru_cache(maxsize=128)
|
138
|
+
def _extract_playlist(resp: str) -> PlayList:
|
139
|
+
""" Playlist answer Format:
|
140
|
+
+----[ Playlist - playlist ]
|
141
|
+
| 1 - Плейлист
|
142
|
+
| 6 - Video 1.mkv (00:23:37) [played 1 time]
|
143
|
+
| *4 - Video 2.mkv (00:23:44) [played 2 times]
|
144
|
+
| 3 - Video 3.mkv (00:23:43) [played 1 time]
|
145
|
+
| 5 - Video 4.mkv (00:23:44) [played 1 time]
|
146
|
+
| 2 - Медиатека
|
147
|
+
| 12 - Video 6.mkv (00:23:44)
|
148
|
+
+----[ End of playlist ]
|
149
|
+
"""
|
150
|
+
|
151
|
+
items: List[PlayListItem] = []
|
152
|
+
active: Optional[PlayListItem] = None
|
153
|
+
|
154
|
+
for idx, match in enumerate(re.finditer(RE_PLAYLIST_ITEM, resp)):
|
155
|
+
item = PlayListItem(idx, match.group(2))
|
156
|
+
items.append(item)
|
157
|
+
if match.group(1) == "*":
|
158
|
+
active = item
|
159
|
+
|
160
|
+
return PlayList(items, active)
|
161
|
+
|
162
|
+
def __repr__(self):
|
163
|
+
return f"Vlc({self.vlc_id}, {self.prev_state=})"
|
164
|
+
|
165
|
+
def close(self):
|
166
|
+
self.vlc_conn.close()
|
167
|
+
|
168
|
+
|
169
|
+
class VlcProcs:
|
170
|
+
def __init__(self, vlc_list_providers: Set[IVlcListFinder]):
|
171
|
+
self.closed = False
|
172
|
+
self._vlc_instances: dict[VlcId, Vlc] = {}
|
173
|
+
self.vlc_list_providers = vlc_list_providers
|
174
|
+
self.vlc_finder_thread = threading.Thread(target=self.refresh_vlc_list_periodically, daemon=True)
|
175
|
+
self.vlc_finder_thread.start()
|
176
|
+
|
177
|
+
def refresh_vlc_list_periodically(self):
|
178
|
+
while not self.closed:
|
179
|
+
start = time.time()
|
180
|
+
|
181
|
+
vlc_candidates = []
|
182
|
+
|
183
|
+
for vlc_list_provider in self.vlc_list_providers:
|
184
|
+
vlc_list_provider: IVlcListFinder
|
185
|
+
next_vlc_ids_list: Set[VlcId] = vlc_list_provider.get_vlc_list()
|
186
|
+
|
187
|
+
vlc_candidates.extend(next_vlc_ids_list)
|
188
|
+
logger.debug(next_vlc_ids_list)
|
189
|
+
|
190
|
+
# Remove missed
|
191
|
+
for orphaned_vlc in (self._vlc_instances.keys() - vlc_candidates):
|
192
|
+
self.dereg(orphaned_vlc)
|
193
|
+
|
194
|
+
# Populate if not exists
|
195
|
+
for vlc_id in vlc_candidates:
|
196
|
+
if vlc_id not in self._vlc_instances.keys():
|
197
|
+
if vlc := self.try_connect(vlc_id):
|
198
|
+
print(f"Found active instance {vlc_id}, with state {vlc.cur_state()}", flush=True)
|
199
|
+
self._vlc_instances[vlc_id] = vlc
|
200
|
+
|
201
|
+
logger.debug(f"Compute all_vlc (took {time.time() - start:.3f})...")
|
202
|
+
time.sleep(5)
|
203
|
+
|
204
|
+
@staticmethod
|
205
|
+
def try_connect(vlc_id):
|
206
|
+
try:
|
207
|
+
return Vlc(vlc_id)
|
208
|
+
except Exception as e:
|
209
|
+
logger.opt(exception=True).debug("Cannot connect to {0}, cause: {1}", vlc_id, e)
|
210
|
+
print(f"Cannot connect to {vlc_id} socket, cause: {e}. Skipping. Enable debug for more info. See --help. ", flush=True)
|
211
|
+
return None
|
212
|
+
|
213
|
+
@property
|
214
|
+
def all_vlc(self) -> dict[VlcId, Vlc]:
|
215
|
+
return self._vlc_instances.copy() # copy: for thread safe
|
216
|
+
|
217
|
+
def sync_all(self, state: State, source_vlc: Vlc):
|
218
|
+
logger.debug(">" * 60)
|
219
|
+
logger.debug(f"Detect change to {state} from {source_vlc.vlc_id}")
|
220
|
+
logger.debug(f" old --> {source_vlc.prev_state} ")
|
221
|
+
logger.debug(f" new --> {state} ")
|
222
|
+
logger.debug(f" Time diff abs(old - new) {abs(source_vlc.prev_state.vid_start_at - state.vid_start_at)}")
|
223
|
+
logger.debug("<" * 60)
|
224
|
+
logger.debug("")
|
225
|
+
print(">>> Sync players...", flush=True)
|
226
|
+
|
227
|
+
for next_pid, next_vlc in self.all_vlc.items():
|
228
|
+
next_vlc: Vlc
|
229
|
+
print(f" Sync {next_pid} to {state}", flush=True)
|
230
|
+
next_vlc.sync_to(state, source_vlc)
|
231
|
+
print()
|
232
|
+
|
233
|
+
def dereg(self, vlc_id: VlcId):
|
234
|
+
print(f"Detect vlc instance closed {vlc_id}", flush=True)
|
235
|
+
if vlc_to_close := self._vlc_instances.pop(vlc_id, None):
|
236
|
+
vlc_to_close.close()
|
237
|
+
|
238
|
+
def close(self):
|
239
|
+
for vlc in self._vlc_instances.values():
|
240
|
+
vlc.close()
|
241
|
+
|
242
|
+
self.closed = True
|
243
|
+
self._vlc_instances.clear()
|
244
|
+
|
245
|
+
def __del__(self):
|
246
|
+
self.close()
|
@@ -28,16 +28,16 @@ class IVlcListFinder:
|
|
28
28
|
|
29
29
|
|
30
30
|
class LocalProcessFinderProvider(IVlcListFinder):
|
31
|
-
def __init__(self, iface):
|
32
|
-
self.
|
31
|
+
def __init__(self, iface: str):
|
32
|
+
self._iface = iface
|
33
33
|
|
34
34
|
def get_vlc_list(self) -> Set[VlcId]:
|
35
35
|
vlc_ports = set()
|
36
36
|
|
37
37
|
for proc in self._find_vlc_procs():
|
38
|
-
port = self._has_listen_port(proc, self.
|
38
|
+
port = self._has_listen_port(proc, self._iface)
|
39
39
|
if port:
|
40
|
-
vlc_ports.add(VlcId(self.
|
40
|
+
vlc_ports.add(VlcId(self._iface, port, proc.pid))
|
41
41
|
|
42
42
|
return vlc_ports
|
43
43
|
|
@@ -90,11 +90,12 @@ class LocalProcessFinderProvider(IVlcListFinder):
|
|
90
90
|
|
91
91
|
def print_exc():
|
92
92
|
print("-" * 60)
|
93
|
-
print("Exception in user code: ")
|
93
|
+
print("Exception in user code: ", flush=True)
|
94
94
|
print("-" * 60)
|
95
95
|
traceback.print_exc(file=sys.stdout)
|
96
96
|
print("-" * 60)
|
97
97
|
print()
|
98
|
+
sys.stdout.flush()
|
98
99
|
|
99
100
|
|
100
101
|
class ExtraHostFinder(IVlcListFinder):
|
@@ -38,16 +38,23 @@ class PlayState(Enum):
|
|
38
38
|
class State:
|
39
39
|
play_state: PlayState
|
40
40
|
seek: int
|
41
|
-
|
41
|
+
playlist_order_idx: int
|
42
|
+
"""
|
43
|
+
vid_start_at - is abs time of video start in given vlc
|
44
|
+
|
45
|
+
If this time changed it indicates time seek
|
46
|
+
|
47
|
+
See for details:
|
48
|
+
- play_in_same_pos()
|
49
|
+
- Vlc.cur_state()
|
50
|
+
"""
|
51
|
+
vid_start_at: float = field(repr=False)
|
42
52
|
|
43
53
|
def same(self, other: State):
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
self.both_stopped(other)
|
49
|
-
)
|
50
|
-
)
|
54
|
+
playlist_same = self.same_playlist_item(other)
|
55
|
+
full_same = (self.same_play_state(other) and playlist_same and (
|
56
|
+
self.play_in_same_pos(other) or self.pause_is_same_pos(other) or self.both_stopped(other)))
|
57
|
+
return full_same, playlist_same
|
51
58
|
|
52
59
|
def same_play_state(self, other: State):
|
53
60
|
return self.play_state == other.play_state
|
@@ -57,23 +64,41 @@ class State:
|
|
57
64
|
|
58
65
|
def play_in_same_pos(self: State, other: State):
|
59
66
|
""" Check time_diff only when play """
|
60
|
-
desync_secs = abs(self.
|
67
|
+
desync_secs = abs(self.vid_start_at - other.vid_start_at)
|
61
68
|
|
62
69
|
if 2 < desync_secs < MAX_DESYNC_SECONDS:
|
63
70
|
logger.debug(f"Asynchronous anomaly between probes: {desync_secs} secs")
|
64
71
|
|
65
72
|
return (
|
66
73
|
self.play_state == other.play_state == PlayState.PLAYING and
|
67
|
-
self.
|
74
|
+
self.vid_start_at and other.vid_start_at and
|
68
75
|
desync_secs < MAX_DESYNC_SECONDS
|
69
76
|
)
|
70
77
|
|
71
78
|
def both_stopped(self, other: State):
|
72
79
|
return self.play_state == other.play_state == PlayState.STOPPED
|
73
80
|
|
74
|
-
def
|
81
|
+
def is_play_or_pause(self):
|
75
82
|
return self.play_state in [PlayState.PLAYING, PlayState.PAUSED]
|
76
83
|
|
84
|
+
def same_playlist_item(self, other):
|
85
|
+
return self.playlist_order_idx == other.playlist_order_idx
|
86
|
+
|
87
|
+
|
88
|
+
@dataclass
|
89
|
+
class PlayListItem:
|
90
|
+
order_index: int
|
91
|
+
vlc_internal_index: int
|
92
|
+
|
93
|
+
|
94
|
+
@dataclass
|
95
|
+
class PlayList:
|
96
|
+
items: List[PlayListItem]
|
97
|
+
active_item: Optional[PlayListItem]
|
98
|
+
|
99
|
+
def active_order_index(self) -> Optional[int]:
|
100
|
+
return self.active_item.order_index if self.active_item else None
|
101
|
+
|
77
102
|
|
78
103
|
@dataclass(frozen=True)
|
79
104
|
class VlcId:
|
vlcsync-0.2.1/setup.py
DELETED
@@ -1,34 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
from setuptools import setup
|
3
|
-
|
4
|
-
packages = \
|
5
|
-
['vlcsync']
|
6
|
-
|
7
|
-
package_data = \
|
8
|
-
{'': ['*']}
|
9
|
-
|
10
|
-
install_requires = \
|
11
|
-
['cached-property', 'click>=8.1.3,<9.0.0', 'loguru', 'psutil']
|
12
|
-
|
13
|
-
entry_points = \
|
14
|
-
{'console_scripts': ['vlcsync = vlcsync.main:main']}
|
15
|
-
|
16
|
-
setup_kwargs = {
|
17
|
-
'name': 'vlcsync',
|
18
|
-
'version': '0.2.1',
|
19
|
-
'description': 'Utility for synchronize multiple instances of VLC. Supports seek, play and pause. ',
|
20
|
-
'long_description': 'VLC Sync\n========\n\nUtility for synchronize multiple instances of VLC. Supports seek, play and pause. \n \n\n#### Motivation\n\nStrongly inspired by F1 streams with extra driver tracking data streams. Did [not find](#alternatives) reasonable alternative for Linux for playing several videos synchronously. So decided to write my own solution.\n\n## Install\n\n```shell\npip3 install -U vlcsync\n```\n\nor \n\n- Download [binary release](https://github.com/mrkeuz/vlcsync/releases) for Windows 7/10 \n NOTE: On some systems there are false positive Antivirus warnings [issues](https://github.com/mrkeuz/vlcsync/issues/1).\n In this case use [alternative way](./docs/install.md#windows-detailed-instructions) to install. \n\n## Run\n\n`Vlc` players should open with `--rc-host 127.0.0.42` option OR configured properly from gui (see [how configure vlc](./docs/vlc_setup.md)) \n\n```shell\n\n# Run vlc players \n$ vlc --rc-host 127.0.0.42 SomeMedia1.mkv &\n$ vlc --rc-host 127.0.0.42 SomeMedia2.mkv &\n$ vlc --rc-host 127.0.0.42 SomeMedia3.mkv &\n\n# vlcsync will monitor and syncing all players\n$ vlcsync\n\n# Started from version 0.2.0\n\n# For control remote vlc instances, \n# remote port should be open and rc interface listen on 0.0.0.0\n$ vlcsync --rc-host 192.168.1.100:12345 --rc-host 192.168.1.50:54321\n\n# For disable local discovery (only remote instances)\n$ vlcsync --no-local-discovery --rc-host 192.168.1.100:12345\n\n# For help and see all options\n$ vlcsync --help\n```\n\n## Awesome \n\nAwesome [use-case](./docs/awesome.md) ideas\n\n## Demo\n\n\n\n## Limitations \n\n- Frame-to-frame sync NOT provided. `vlc` does not have precise controlling via `rc` interface out of box. \n Difference between videos can be **up to ~0.5 seconds** in worst case. Especially when playing from network share, \n due buffering time and network latency.\n\n- Currently, tested only on:\n - Linux (Ubuntu 20.04)\n - Windows 7 (32-bit)\n - Windows 10 (64-bit)\n\n## Alternatives\n\n- [vlc](https://www.videolan.org/vlc/index.ru.html) \n - There is a [netsync](https://wiki.videolan.org/Documentation:Modules/netsync/) but seem only master-slave (tried, but not working by some reason)\n - Open additional media. Seems feature broken in vlc 3 (also afaik limited only 2 streams) \n- [Syncplay](https://github.com/Syncplay/syncplay) - very promised, but little [complicated](https://github.com/Syncplay/syncplay/discussions/463) for sync different videos\n- [bino](https://bino3d.org/) - working, very strange controls, file dialog not working and only fullscreen\n- [gridplayer](https://github.com/vzhd1701/gridplayer) - low fps by some reason\n- [mpv](https://github.com/mpv-player/mpv) - with [mixing multiple videos](https://superuser.com/a/1325668/1272472) in one window. Unfortunally does not support multiple screens\n- [AVPlayer](http://www.awesomevideoplayer.com/) - only Win, macOS, up to 4 videos in free version\n\n## Contributing\n\nAny thoughts, ideas and contributions welcome!\n\nA special thanks to **KorDen32** for inspiration! <img src="./docs/F1.svg" alt="F1" width="45"/>\n\nEnjoy!\n',
|
21
|
-
'author': 'mrkeuz',
|
22
|
-
'author_email': 'mrkeuz@users.noreply.github.com',
|
23
|
-
'maintainer': None,
|
24
|
-
'maintainer_email': None,
|
25
|
-
'url': 'https://github.com/mrkeuz/vlcsync/',
|
26
|
-
'packages': packages,
|
27
|
-
'package_data': package_data,
|
28
|
-
'install_requires': install_requires,
|
29
|
-
'entry_points': entry_points,
|
30
|
-
'python_requires': '>=3.8,<4.0',
|
31
|
-
}
|
32
|
-
|
33
|
-
|
34
|
-
setup(**setup_kwargs)
|
vlcsync-0.2.1/vlcsync/syncer.py
DELETED
@@ -1,84 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
from dataclasses import dataclass
|
4
|
-
import sys
|
5
|
-
import time
|
6
|
-
from typing import Set
|
7
|
-
|
8
|
-
from loguru import logger
|
9
|
-
|
10
|
-
from vlcsync.vlc import VLC_IFACE_IP, VlcProcs
|
11
|
-
from vlcsync.vlc_finder import LocalProcessFinderProvider, ExtraHostFinder
|
12
|
-
from vlcsync.vlc_socket import VlcConnectionError
|
13
|
-
|
14
|
-
from vlcsync.vlc_state import VlcId
|
15
|
-
|
16
|
-
|
17
|
-
@dataclass
|
18
|
-
class AppConfig:
|
19
|
-
extra_rc_hosts: Set[VlcId]
|
20
|
-
no_local_discovery: bool
|
21
|
-
|
22
|
-
|
23
|
-
class Syncer:
|
24
|
-
def __init__(self, app_config: AppConfig):
|
25
|
-
self.env = None
|
26
|
-
self.app_config = app_config
|
27
|
-
self.supress_log_until = 0
|
28
|
-
|
29
|
-
vlc_finders = set()
|
30
|
-
if not self.app_config.no_local_discovery:
|
31
|
-
vlc_finders.add(LocalProcessFinderProvider(VLC_IFACE_IP))
|
32
|
-
print(f" Discover instances on {VLC_IFACE_IP} iface...")
|
33
|
-
else:
|
34
|
-
print(" Local discovery vlc instances DISABLED...")
|
35
|
-
|
36
|
-
if app_config.extra_rc_hosts:
|
37
|
-
vlc_finders.add(ExtraHostFinder(app_config.extra_rc_hosts))
|
38
|
-
for rc_host in app_config.extra_rc_hosts:
|
39
|
-
rc_host: VlcId
|
40
|
-
print(f" Manual host defined {rc_host.addr}:{rc_host.port}")
|
41
|
-
else:
|
42
|
-
print(""" Manual vlc addresses ("--rc-host" args) NOT provided...""")
|
43
|
-
|
44
|
-
if not vlc_finders:
|
45
|
-
print("\nTarget vlc instances not selected (nor autodiscover, nor manually). \n"
|
46
|
-
"""See: "vlcsync --help" for more info""")
|
47
|
-
sys.exit(1)
|
48
|
-
|
49
|
-
self.env = VlcProcs(vlc_finders)
|
50
|
-
|
51
|
-
def __enter__(self):
|
52
|
-
self.do_sync()
|
53
|
-
return self
|
54
|
-
|
55
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
56
|
-
self.close()
|
57
|
-
|
58
|
-
def do_sync(self):
|
59
|
-
self.log_with_debounce("Sync...")
|
60
|
-
try:
|
61
|
-
for vlc_id, vlc in self.env.all_vlc.items():
|
62
|
-
is_changed, state = vlc.is_state_change()
|
63
|
-
if not state.is_active():
|
64
|
-
continue
|
65
|
-
|
66
|
-
if is_changed:
|
67
|
-
print(f"\nVlc state change detected from ({vlc_id})")
|
68
|
-
self.env.sync_all(state, vlc)
|
69
|
-
return
|
70
|
-
|
71
|
-
except VlcConnectionError as e:
|
72
|
-
self.env.dereg(e.vlc_id)
|
73
|
-
|
74
|
-
def log_with_debounce(self, msg: str, _debounce=5):
|
75
|
-
if time.time() > self.supress_log_until:
|
76
|
-
logger.debug(msg)
|
77
|
-
self.supress_log_until = time.time() + _debounce
|
78
|
-
|
79
|
-
def __del__(self):
|
80
|
-
self.close()
|
81
|
-
|
82
|
-
def close(self):
|
83
|
-
if self.env:
|
84
|
-
self.env.close()
|
vlcsync-0.2.1/vlcsync/vlc.py
DELETED
@@ -1,176 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import socket
|
4
|
-
import threading
|
5
|
-
import time
|
6
|
-
from typing import Set
|
7
|
-
|
8
|
-
from loguru import logger
|
9
|
-
|
10
|
-
from vlcsync.vlc_finder import IVlcListFinder
|
11
|
-
from vlcsync.vlc_socket import VlcSocket
|
12
|
-
from vlcsync.vlc_state import PlayState, State, VlcId
|
13
|
-
|
14
|
-
VLC_IFACE_IP = "127.0.0.42"
|
15
|
-
|
16
|
-
socket.setdefaulttimeout(0.5)
|
17
|
-
|
18
|
-
|
19
|
-
class Vlc:
|
20
|
-
def __init__(self, vlc_id: VlcId):
|
21
|
-
self.vlc_id = vlc_id
|
22
|
-
self.vlc_conn = VlcSocket(vlc_id)
|
23
|
-
self.prev_state: State = self.cur_state()
|
24
|
-
|
25
|
-
def play_state(self) -> PlayState:
|
26
|
-
status = self.vlc_conn.cmd("status")
|
27
|
-
return self._extract_state(status)
|
28
|
-
|
29
|
-
def get_time(self) -> int | None:
|
30
|
-
seek = self.vlc_conn.cmd("get_time")
|
31
|
-
if seek != '':
|
32
|
-
return int(seek)
|
33
|
-
|
34
|
-
def seek(self, seek: int):
|
35
|
-
self.vlc_conn.cmd(f"seek {seek}")
|
36
|
-
|
37
|
-
def cur_state(self) -> State:
|
38
|
-
get_time = self.get_time()
|
39
|
-
|
40
|
-
return State(self.play_state(),
|
41
|
-
get_time,
|
42
|
-
time.time() - (get_time or 0))
|
43
|
-
|
44
|
-
def is_state_change(self) -> (bool, State):
|
45
|
-
cur_state: State = self.cur_state()
|
46
|
-
prev_state: State = self.prev_state
|
47
|
-
is_change = not cur_state.same(prev_state)
|
48
|
-
|
49
|
-
# Return cur_state for reduce further socket communications
|
50
|
-
return is_change, cur_state
|
51
|
-
|
52
|
-
def sync_to(self, new_state: State, source: Vlc):
|
53
|
-
if not new_state.is_active():
|
54
|
-
return
|
55
|
-
|
56
|
-
cur_play_state = self.play_state()
|
57
|
-
|
58
|
-
if cur_play_state != new_state.play_state:
|
59
|
-
self.play_if_pause(new_state, cur_play_state)
|
60
|
-
self.pause_if_play(new_state, cur_play_state)
|
61
|
-
|
62
|
-
# Sync all secondary, but main only when playing
|
63
|
-
if source != self or cur_play_state == PlayState.PLAYING:
|
64
|
-
self.seek(new_state.seek)
|
65
|
-
|
66
|
-
self.prev_state = self.cur_state()
|
67
|
-
|
68
|
-
def pause_if_play(self, new_state, cur_play_state):
|
69
|
-
if cur_play_state == PlayState.PLAYING and new_state.play_state == PlayState.PAUSED:
|
70
|
-
self.vlc_conn.cmd("pause")
|
71
|
-
|
72
|
-
def play_if_pause(self, new_state, cur_play_state):
|
73
|
-
if cur_play_state == PlayState.PAUSED and new_state.play_state == PlayState.PLAYING:
|
74
|
-
self.vlc_conn.cmd("play")
|
75
|
-
|
76
|
-
@staticmethod
|
77
|
-
def _extract_state(status, _valid_states=(PlayState.PLAYING.value,
|
78
|
-
PlayState.PAUSED.value,
|
79
|
-
PlayState.STOPPED.value)):
|
80
|
-
for pb_state in _valid_states:
|
81
|
-
if pb_state in status:
|
82
|
-
return PlayState(pb_state)
|
83
|
-
|
84
|
-
return PlayState.UNKNOWN
|
85
|
-
|
86
|
-
def __repr__(self):
|
87
|
-
return f"Vlc({self.vlc_id}, {self.prev_state=})"
|
88
|
-
|
89
|
-
def close(self):
|
90
|
-
self.vlc_conn.close()
|
91
|
-
|
92
|
-
|
93
|
-
class VlcProcs:
|
94
|
-
def __init__(self, vlc_list_providers: Set[IVlcListFinder]):
|
95
|
-
self.closed = False
|
96
|
-
self._vlc_instances: dict[VlcId, Vlc] = {}
|
97
|
-
self.vlc_list_providers = vlc_list_providers
|
98
|
-
self.vlc_finder_thread = threading.Thread(target=self.refresh_vlc_list_periodically, daemon=True)
|
99
|
-
self.vlc_finder_thread.start()
|
100
|
-
|
101
|
-
def refresh_vlc_list_periodically(self):
|
102
|
-
while not self.closed:
|
103
|
-
start = time.time()
|
104
|
-
|
105
|
-
vlc_candidates = []
|
106
|
-
|
107
|
-
for vlc_list_provider in self.vlc_list_providers:
|
108
|
-
vlc_list_provider: IVlcListFinder
|
109
|
-
next_vlc_ids_list: Set[VlcId] = vlc_list_provider.get_vlc_list()
|
110
|
-
|
111
|
-
vlc_candidates.extend(next_vlc_ids_list)
|
112
|
-
logger.debug(next_vlc_ids_list)
|
113
|
-
|
114
|
-
# Remove missed
|
115
|
-
for orphaned_vlc in (self._vlc_instances.keys() - vlc_candidates):
|
116
|
-
self.dereg(orphaned_vlc)
|
117
|
-
|
118
|
-
# Populate if not exists
|
119
|
-
for vlc_id in vlc_candidates:
|
120
|
-
if vlc_id not in self._vlc_instances.keys():
|
121
|
-
if vlc := self.try_connect(vlc_id):
|
122
|
-
print(f"Found active instance {vlc_id}, with state {vlc.cur_state()}")
|
123
|
-
self._vlc_instances[vlc_id] = vlc
|
124
|
-
|
125
|
-
logger.debug(f"Compute all_vlc (took {time.time() - start:.3f})...")
|
126
|
-
time.sleep(5)
|
127
|
-
|
128
|
-
@staticmethod
|
129
|
-
def try_connect(vlc_id):
|
130
|
-
try:
|
131
|
-
return Vlc(vlc_id)
|
132
|
-
except Exception as e:
|
133
|
-
logger.opt(exception=True).debug("Cannot connect to {0}, cause: {1}", vlc_id, e)
|
134
|
-
print(f"Cannot connect to {vlc_id} socket, cause: {e}. Skipping. Enable debug for more info. See --help. ")
|
135
|
-
return None
|
136
|
-
|
137
|
-
@property
|
138
|
-
def all_vlc(self) -> dict[VlcId, Vlc]:
|
139
|
-
return self._vlc_instances.copy() # copy: for thread safe
|
140
|
-
|
141
|
-
def sync_all(self, state: State, source: Vlc):
|
142
|
-
logger.debug(">" * 60)
|
143
|
-
logger.debug(f"Detect change to {state} from {source.vlc_id}")
|
144
|
-
logger.debug(f" old --> {source.prev_state} ")
|
145
|
-
logger.debug(f" new --> {state} ")
|
146
|
-
logger.debug(f" Time diff abs(old - new) {abs(state.time_diff - source.prev_state.time_diff)}")
|
147
|
-
logger.debug("<" * 60)
|
148
|
-
logger.debug("")
|
149
|
-
print(">>> Sync windows...")
|
150
|
-
if not state.is_active():
|
151
|
-
print(" Source window stopped. Skip sync")
|
152
|
-
return
|
153
|
-
|
154
|
-
for next_pid, next_vlc in self.all_vlc.items():
|
155
|
-
next_vlc: Vlc
|
156
|
-
if next_vlc.cur_state().is_active():
|
157
|
-
print(f" Sync {next_pid} to {state}")
|
158
|
-
next_vlc.sync_to(state, source)
|
159
|
-
print()
|
160
|
-
|
161
|
-
def dereg(self, vlc_id: VlcId):
|
162
|
-
print(f"Detect vlc instance closed {vlc_id}")
|
163
|
-
if vlc_to_close := self._vlc_instances.pop(vlc_id, None):
|
164
|
-
vlc_to_close.close()
|
165
|
-
|
166
|
-
def close(self):
|
167
|
-
for vlc in self._vlc_instances.values():
|
168
|
-
vlc.close()
|
169
|
-
|
170
|
-
self.closed = True
|
171
|
-
self._vlc_instances.clear()
|
172
|
-
|
173
|
-
def __del__(self):
|
174
|
-
self.close()
|
175
|
-
|
176
|
-
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|