vlcsync 0.2.1__py3-none-any.whl → 0.3.1__py3-none-any.whl
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/cli.py +19 -6
- vlcsync/syncer.py +48 -18
- vlcsync/vlc.py +118 -48
- vlcsync/vlc_finder.py +6 -5
- vlcsync/vlc_state.py +36 -11
- {vlcsync-0.2.1.dist-info → vlcsync-0.3.1.dist-info}/METADATA +8 -4
- vlcsync-0.3.1.dist-info/RECORD +14 -0
- vlcsync-0.2.1.dist-info/RECORD +0 -14
- {vlcsync-0.2.1.dist-info → vlcsync-0.3.1.dist-info}/LICENSE.md +0 -0
- {vlcsync-0.2.1.dist-info → vlcsync-0.3.1.dist-info}/WHEEL +0 -0
- {vlcsync-0.2.1.dist-info → vlcsync-0.3.1.dist-info}/entry_points.txt +0 -0
vlcsync/cli.py
CHANGED
@@ -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)
|
vlcsync/syncer.py
CHANGED
@@ -3,11 +3,11 @@ from __future__ import annotations
|
|
3
3
|
from dataclasses import dataclass
|
4
4
|
import sys
|
5
5
|
import time
|
6
|
-
from typing import Set
|
6
|
+
from typing import Set, List
|
7
7
|
|
8
8
|
from loguru import logger
|
9
9
|
|
10
|
-
from vlcsync.vlc import VLC_IFACE_IP, VlcProcs
|
10
|
+
from vlcsync.vlc import VLC_IFACE_IP, VlcProcs, Vlc
|
11
11
|
from vlcsync.vlc_finder import LocalProcessFinderProvider, ExtraHostFinder
|
12
12
|
from vlcsync.vlc_socket import VlcConnectionError
|
13
13
|
|
@@ -18,6 +18,7 @@ from vlcsync.vlc_state import VlcId
|
|
18
18
|
class AppConfig:
|
19
19
|
extra_rc_hosts: Set[VlcId]
|
20
20
|
no_local_discovery: bool
|
21
|
+
volume_sync: bool
|
21
22
|
|
22
23
|
|
23
24
|
class Syncer:
|
@@ -29,48 +30,77 @@ class Syncer:
|
|
29
30
|
vlc_finders = set()
|
30
31
|
if not self.app_config.no_local_discovery:
|
31
32
|
vlc_finders.add(LocalProcessFinderProvider(VLC_IFACE_IP))
|
32
|
-
print(f" Discover instances on {VLC_IFACE_IP} iface...")
|
33
|
+
print(f" Discover instances on {VLC_IFACE_IP} iface...", flush=True)
|
33
34
|
else:
|
34
|
-
print(" Local discovery vlc instances DISABLED...")
|
35
|
+
print(" Local discovery vlc instances DISABLED...", flush=True)
|
35
36
|
|
36
37
|
if app_config.extra_rc_hosts:
|
37
38
|
vlc_finders.add(ExtraHostFinder(app_config.extra_rc_hosts))
|
38
39
|
for rc_host in app_config.extra_rc_hosts:
|
39
40
|
rc_host: VlcId
|
40
|
-
print(f" Manual host defined {rc_host.addr}:{rc_host.port}")
|
41
|
+
print(f" Manual host defined {rc_host.addr}:{rc_host.port}", flush=True)
|
41
42
|
else:
|
42
|
-
print(""" Manual vlc addresses ("--rc-host" args) NOT provided...""")
|
43
|
+
print(""" Manual vlc addresses ("--rc-host" args) NOT provided...""", flush=True)
|
43
44
|
|
44
45
|
if not vlc_finders:
|
45
46
|
print("\nTarget vlc instances not selected (nor autodiscover, nor manually). \n"
|
46
|
-
"""See: "vlcsync --help" for more info""")
|
47
|
+
"""See: "vlcsync --help" for more info""", flush=True)
|
47
48
|
sys.exit(1)
|
48
49
|
|
49
50
|
self.env = VlcProcs(vlc_finders)
|
50
51
|
|
51
52
|
def __enter__(self):
|
52
|
-
self.
|
53
|
+
self.do_check_synchronized()
|
53
54
|
return self
|
54
55
|
|
55
56
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
56
57
|
self.close()
|
57
58
|
|
58
|
-
def
|
59
|
-
self.log_with_debounce("
|
59
|
+
def do_check_synchronized(self):
|
60
|
+
self.log_with_debounce("do_check_synchronized()...")
|
60
61
|
try:
|
61
|
-
|
62
|
-
|
63
|
-
if not state.is_active():
|
64
|
-
continue
|
62
|
+
if self.app_config.volume_sync:
|
63
|
+
self.sync_volume()
|
65
64
|
|
66
|
-
|
67
|
-
print(f"\nVlc state change detected from ({vlc_id})")
|
68
|
-
self.env.sync_all(state, vlc)
|
69
|
-
return
|
65
|
+
self.sync_playstate()
|
70
66
|
|
71
67
|
except VlcConnectionError as e:
|
72
68
|
self.env.dereg(e.vlc_id)
|
73
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
|
+
|
74
104
|
def log_with_debounce(self, msg: str, _debounce=5):
|
75
105
|
if time.time() > self.supress_log_until:
|
76
106
|
logger.debug(msg)
|
vlcsync/vlc.py
CHANGED
@@ -1,17 +1,21 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
from functools import lru_cache
|
4
|
+
import re
|
3
5
|
import socket
|
4
6
|
import threading
|
5
7
|
import time
|
6
|
-
from typing import Set
|
8
|
+
from typing import Set, List, Optional
|
7
9
|
|
8
10
|
from loguru import logger
|
9
11
|
|
10
12
|
from vlcsync.vlc_finder import IVlcListFinder
|
11
13
|
from vlcsync.vlc_socket import VlcSocket
|
12
|
-
from vlcsync.vlc_state import PlayState, State, VlcId
|
14
|
+
from vlcsync.vlc_state import PlayState, State, VlcId, PlayList, PlayListItem
|
13
15
|
|
14
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+) - ')
|
15
19
|
|
16
20
|
socket.setdefaulttimeout(0.5)
|
17
21
|
|
@@ -21,67 +25,139 @@ class Vlc:
|
|
21
25
|
self.vlc_id = vlc_id
|
22
26
|
self.vlc_conn = VlcSocket(vlc_id)
|
23
27
|
self.prev_state: State = self.cur_state()
|
28
|
+
self.prev_volume = self.volume()
|
24
29
|
|
25
30
|
def play_state(self) -> PlayState:
|
26
31
|
status = self.vlc_conn.cmd("status")
|
27
32
|
return self._extract_state(status)
|
28
33
|
|
29
|
-
def
|
34
|
+
def get_seek(self) -> int | None:
|
30
35
|
seek = self.vlc_conn.cmd("get_time")
|
31
36
|
if seek != '':
|
32
37
|
return int(seek)
|
33
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
|
+
|
34
57
|
def seek(self, seek: int):
|
35
58
|
self.vlc_conn.cmd(f"seek {seek}")
|
36
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
|
+
|
37
69
|
def cur_state(self) -> State:
|
38
|
-
|
70
|
+
cur_seek = self.get_seek()
|
39
71
|
|
40
72
|
return State(self.play_state(),
|
41
|
-
|
42
|
-
|
73
|
+
cur_seek,
|
74
|
+
self.playlist().active_order_index(),
|
75
|
+
# Abs time of video start
|
76
|
+
time.time() - (cur_seek or 0)
|
77
|
+
)
|
43
78
|
|
44
79
|
def is_state_change(self) -> (bool, State):
|
45
80
|
cur_state: State = self.cur_state()
|
46
81
|
prev_state: State = self.prev_state
|
47
|
-
|
82
|
+
full_same, playlist_same = cur_state.same(prev_state)
|
48
83
|
|
49
84
|
# Return cur_state for reduce further socket communications
|
50
|
-
return
|
85
|
+
return not full_same, cur_state, not playlist_same
|
51
86
|
|
52
87
|
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
88
|
|
58
|
-
|
59
|
-
|
60
|
-
|
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)
|
89
|
+
self._sync_playlist(new_state)
|
90
|
+
self._sync_playstate(new_state)
|
91
|
+
self._sync_timeline(new_state, source)
|
65
92
|
|
66
93
|
self.prev_state = self.cur_state()
|
67
94
|
|
68
|
-
def
|
69
|
-
|
70
|
-
|
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)
|
71
107
|
|
72
|
-
def
|
73
|
-
|
74
|
-
|
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()
|
75
129
|
|
76
130
|
@staticmethod
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
if pb_state in status:
|
82
|
-
return PlayState(pb_state)
|
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
|
83
135
|
|
84
|
-
|
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)
|
85
161
|
|
86
162
|
def __repr__(self):
|
87
163
|
return f"Vlc({self.vlc_id}, {self.prev_state=})"
|
@@ -119,7 +195,7 @@ class VlcProcs:
|
|
119
195
|
for vlc_id in vlc_candidates:
|
120
196
|
if vlc_id not in self._vlc_instances.keys():
|
121
197
|
if vlc := self.try_connect(vlc_id):
|
122
|
-
print(f"Found active instance {vlc_id}, with state {vlc.cur_state()}")
|
198
|
+
print(f"Found active instance {vlc_id}, with state {vlc.cur_state()}", flush=True)
|
123
199
|
self._vlc_instances[vlc_id] = vlc
|
124
200
|
|
125
201
|
logger.debug(f"Compute all_vlc (took {time.time() - start:.3f})...")
|
@@ -131,35 +207,31 @@ class VlcProcs:
|
|
131
207
|
return Vlc(vlc_id)
|
132
208
|
except Exception as e:
|
133
209
|
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. ")
|
210
|
+
print(f"Cannot connect to {vlc_id} socket, cause: {e}. Skipping. Enable debug for more info. See --help. ", flush=True)
|
135
211
|
return None
|
136
212
|
|
137
213
|
@property
|
138
214
|
def all_vlc(self) -> dict[VlcId, Vlc]:
|
139
215
|
return self._vlc_instances.copy() # copy: for thread safe
|
140
216
|
|
141
|
-
def sync_all(self, state: State,
|
217
|
+
def sync_all(self, state: State, source_vlc: Vlc):
|
142
218
|
logger.debug(">" * 60)
|
143
|
-
logger.debug(f"Detect change to {state} from {
|
144
|
-
logger.debug(f" old --> {
|
219
|
+
logger.debug(f"Detect change to {state} from {source_vlc.vlc_id}")
|
220
|
+
logger.debug(f" old --> {source_vlc.prev_state} ")
|
145
221
|
logger.debug(f" new --> {state} ")
|
146
|
-
logger.debug(f" Time diff abs(old - new) {abs(
|
222
|
+
logger.debug(f" Time diff abs(old - new) {abs(source_vlc.prev_state.vid_start_at - state.vid_start_at)}")
|
147
223
|
logger.debug("<" * 60)
|
148
224
|
logger.debug("")
|
149
|
-
print(">>> Sync
|
150
|
-
if not state.is_active():
|
151
|
-
print(" Source window stopped. Skip sync")
|
152
|
-
return
|
225
|
+
print(">>> Sync players...", flush=True)
|
153
226
|
|
154
227
|
for next_pid, next_vlc in self.all_vlc.items():
|
155
228
|
next_vlc: Vlc
|
156
|
-
|
157
|
-
|
158
|
-
next_vlc.sync_to(state, source)
|
229
|
+
print(f" Sync {next_pid} to {state}", flush=True)
|
230
|
+
next_vlc.sync_to(state, source_vlc)
|
159
231
|
print()
|
160
232
|
|
161
233
|
def dereg(self, vlc_id: VlcId):
|
162
|
-
print(f"Detect vlc instance closed {vlc_id}")
|
234
|
+
print(f"Detect vlc instance closed {vlc_id}", flush=True)
|
163
235
|
if vlc_to_close := self._vlc_instances.pop(vlc_id, None):
|
164
236
|
vlc_to_close.close()
|
165
237
|
|
@@ -172,5 +244,3 @@ class VlcProcs:
|
|
172
244
|
|
173
245
|
def __del__(self):
|
174
246
|
self.close()
|
175
|
-
|
176
|
-
|
vlcsync/vlc_finder.py
CHANGED
@@ -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):
|
vlcsync/vlc_state.py
CHANGED
@@ -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:
|
@@ -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
|
@@ -0,0 +1,14 @@
|
|
1
|
+
vlcsync/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
vlcsync/cli.py,sha256=ssoMtW_DorOs0QK5-mEqm6itQQQVci86B8OA2Ed4Nw0,2005
|
3
|
+
vlcsync/cli_utils.py,sha256=hXSljymGPU5AzFr706Ea-81wxkVCYLhZY-ug-JExZ0E,883
|
4
|
+
vlcsync/main.py,sha256=An0NQ4jy3mfygwxko_UA3Sdwri9yidcUTTCP3SoyrpI,303
|
5
|
+
vlcsync/syncer.py,sha256=x9w0LWmCUtFlMFTi01q8BBos0d9fwUpOQMo0dko6n_0,3854
|
6
|
+
vlcsync/vlc.py,sha256=dBrg2TYQswhewtUveBilp0jadfMQAMRZkqgBukUK3F0,8528
|
7
|
+
vlcsync/vlc_finder.py,sha256=7BXhOeJG8lRqYJ7dbrLU3Ps8ghUPh666YkAZjNom-cw,2770
|
8
|
+
vlcsync/vlc_socket.py,sha256=WQAtNoitc49sst0oRG0bEuTkgw6a2TJXVE9iSWQaMa0,1850
|
9
|
+
vlcsync/vlc_state.py,sha256=8RpmxVS_zijqYw9rxpmnN5QwM3Sk1FmqgTxgCH3U8kU,3133
|
10
|
+
vlcsync-0.3.1.dist-info/entry_points.txt,sha256=2m_39oATnFzf5kuvlhkayyWSmW0tUefyNk9dVP4A7B0,45
|
11
|
+
vlcsync-0.3.1.dist-info/LICENSE.md,sha256=Q6ik40IpumjnuWfS1c69jnEyxAPTBV2v04DesGSgLPM,1074
|
12
|
+
vlcsync-0.3.1.dist-info/WHEEL,sha256=DA86_h4QwwzGeRoz62o1svYt5kGEXpoUTuTtwzoTb30,83
|
13
|
+
vlcsync-0.3.1.dist-info/METADATA,sha256=UWdLvYjpLAGk9KC_0pGnDrpF9dEdwZM6qs9cmrj-qrs,4258
|
14
|
+
vlcsync-0.3.1.dist-info/RECORD,,
|
vlcsync-0.2.1.dist-info/RECORD
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
vlcsync/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
vlcsync/cli.py,sha256=y-NXVFVEc92PYSz6S1qZRn8t_RYtH8SK7FxEd6bAsuU,1382
|
3
|
-
vlcsync/cli_utils.py,sha256=hXSljymGPU5AzFr706Ea-81wxkVCYLhZY-ug-JExZ0E,883
|
4
|
-
vlcsync/main.py,sha256=An0NQ4jy3mfygwxko_UA3Sdwri9yidcUTTCP3SoyrpI,303
|
5
|
-
vlcsync/syncer.py,sha256=_x5Lm3iOALgqukEyXi1R8-CJrGHp6vGFm8pKRX8YKwU,2503
|
6
|
-
vlcsync/vlc.py,sha256=InpH8u9f2dG6jw-LerrJSygMvboyk84hksO7-QBh5Iw,5856
|
7
|
-
vlcsync/vlc_finder.py,sha256=8cc1IktUqmaY8xjWjgO6uVkpvtb7tAwPOlPBck1-oZY,2727
|
8
|
-
vlcsync/vlc_socket.py,sha256=WQAtNoitc49sst0oRG0bEuTkgw6a2TJXVE9iSWQaMa0,1850
|
9
|
-
vlcsync/vlc_state.py,sha256=Y2jtVO9vHIJCqOdPzBAM9TC3e4R_wxhgYQsYwOgbi7U,2442
|
10
|
-
vlcsync-0.2.1.dist-info/entry_points.txt,sha256=2m_39oATnFzf5kuvlhkayyWSmW0tUefyNk9dVP4A7B0,45
|
11
|
-
vlcsync-0.2.1.dist-info/LICENSE.md,sha256=Q6ik40IpumjnuWfS1c69jnEyxAPTBV2v04DesGSgLPM,1074
|
12
|
-
vlcsync-0.2.1.dist-info/WHEEL,sha256=DA86_h4QwwzGeRoz62o1svYt5kGEXpoUTuTtwzoTb30,83
|
13
|
-
vlcsync-0.2.1.dist-info/METADATA,sha256=TCCRF2Fynjgiolg_sNFi9ItcFX3fN0ca7omY5Dgw-X4,4121
|
14
|
-
vlcsync-0.2.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|