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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vlcsync
3
- Version: 0.2.1
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 only on:
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. Unfortunally does not support multiple screens
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 only on:
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. Unfortunally does not support multiple screens
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
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "vlcsync"
3
- version = "0.2.1"
3
+ version = "0.3.1"
4
4
  description = "Utility for synchronize multiple instances of VLC. Supports seek, play and pause. "
5
5
  authors = ["mrkeuz <mrkeuz@users.noreply.github.com>"]
6
6
  license = "MIT"
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![vlcsync](./docs/vlcsync.gif)\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
- __version__ = "0.2.1"
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
- def main(rc_host_list: Set[VlcId], no_local_discover):
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.do_sync()
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.iface = iface
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.iface)
38
+ port = self._has_listen_port(proc, self._iface)
39
39
  if port:
40
- vlc_ports.add(VlcId(self.iface, port, proc.pid))
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
- time_diff: float = field(repr=False)
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
- return (self.same_play_state(other) and
45
- (
46
- self.play_in_same_pos(other) or
47
- self.pause_is_same_pos(other) or
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.time_diff - other.time_diff)
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.time_diff and other.time_diff and
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 is_active(self):
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![vlcsync](./docs/vlcsync.gif)\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)
@@ -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()
@@ -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