Skip to content

Commit 8f10ce9

Browse files
committed
Store libVLC stats in debug mode
1 parent ec37eb1 commit 8f10ce9

File tree

4 files changed

+76
-4
lines changed

4 files changed

+76
-4
lines changed

iblrig_custom_tasks/_sp_passiveVideo/__init__.py

Whitespace-only changes.

iblrig_custom_tasks/_sp_passiveVideo/task.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import time
77
from pathlib import Path
88
from collections import defaultdict
9+
from functools import partial
910
import logging
1011
import warnings
1112

@@ -27,6 +28,18 @@
2728
'git+https://github.com/int-brain-lab/project_extraction.git"', RuntimeWarning)
2829

2930

31+
class MediaStats(vlc.MediaStats):
32+
"""A class to store media stats."""
33+
34+
def fieldnames(self):
35+
"""Return the field names."""
36+
return zip(*self._fields_)[0]
37+
38+
def as_tuple(self):
39+
"""Return all attribute values as a tuple."""
40+
return tuple(map(partial(getattr, self), self.fieldnames()))
41+
42+
3043
class Player:
3144
"""A VLC player."""
3245
def __init__(self, rate=1):
@@ -35,6 +48,8 @@ def __init__(self, rate=1):
3548
self._player.set_fullscreen(True)
3649
self._player.set_rate(rate)
3750
self._media = None
51+
self._media_stats = MediaStats()
52+
self._stats = []
3853
self.events = defaultdict(list)
3954
em = self._player.event_manager()
4055
for event in (vlc.EventType.MediaPlayerPlaying, vlc.EventType.MediaPlayerEndReached):
@@ -46,6 +61,27 @@ def _record_event(self, event):
4661
# Have to convert to str as object pointer may change
4762
self.events[str(event.type).split('.')[-1]].append(time.time())
4863

64+
def update_media_stats(self):
65+
"""Update media stats.
66+
67+
Returns
68+
-------
69+
bool
70+
True if the stats have changed since the last update.
71+
"""
72+
if not vlc.libvlc_media_get_stats(self._player.get_media(), self._media_stats):
73+
return False
74+
stats = tuple((time.time(), *self._media_stats.as_tuple()))
75+
if not any(self._stats) or stats[1:] != self._stats[-1][1:]:
76+
self._stats.append(stats)
77+
return True
78+
return False
79+
80+
@property
81+
def stats(self):
82+
"""Return media stats."""
83+
return pd.DataFrame(self._stats, columns=['time', *self._media_stats.fieldnames()])
84+
4985
def play(self, path):
5086
"""Play a video.
5187
@@ -112,8 +148,10 @@ def __init__(self, **kwargs):
112148
if self.hardware_settings.get('MAIN_SYNC', False):
113149
raise NotImplementedError('Recording frame2ttl on Bpod not yet implemented')
114150
self.paths.DATA_FILE_PATH = self.paths.DATA_FILE_PATH.with_name('_sp_taskData.raw.pqt')
151+
self.paths.STATS_FILE_PATH = self.paths.DATA_FILE_PATH.with_name('_sp_videoData.stats.pqt')
115152
self.video = None
116153
self.trial_num = -1
154+
self._log_level = logging.getLevelNamesMapping()[kwargs.get('log_level', 'INFO')]
117155
columns = ['intervals_0', 'intervals_1']
118156
self.data = pd.DataFrame(pd.NA, index=range(self.task_params.NREPEATS), columns=columns)
119157

@@ -122,10 +160,13 @@ def save(self):
122160
if self.video:
123161
data = pd.concat([self.data, pd.DataFrame.from_dict(self.video.events)], axis=1)
124162
data.to_parquet(self.paths.DATA_FILE_PATH)
163+
if 20 > self._log_level > 0:
164+
stats = self.video.stats
165+
stats.to_parquet(self.paths.STATS_FILE_PATH)
125166
self.paths.SESSION_FOLDER.joinpath('transfer_me.flag').touch()
126167

127168
def start_hardware(self):
128-
self.start_mixin_bpod() # used for protocol spacer only
169+
self.start_mixin_bpod()
129170
self.video = Player()
130171

131172
def next_trial(self):
@@ -150,15 +191,15 @@ def _set_bpod_out(self, val):
150191

151192
def _run(self):
152193
"""This is the method that runs the video."""
153-
# make the bpod send spacer signals to the main sync clock for protocol discovery
154-
self.send_spacers()
155194
for rep in range(self.task_params.NREPEATS): # Main loop
156195
self.next_trial()
157196
self._set_bpod_out(True)
158197
# TODO c.f. MediaListPlayerPlayed event
159198
while not self.video.is_started:
160199
... # takes time to actually start playback
161200
while self.video.is_playing or (end_time := self.video.get_ended_time(rep)) is None:
201+
if 20 > self._log_level > 0:
202+
self.video.update_media_stats()
162203
time.sleep(0.05)
163204
# trial finishes when playback finishes
164205
self._set_bpod_out(False)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import unittest
2+
from unittest.mock import Mock
3+
from iblrig_custom_tasks._sp_passiveVideo.task import Session, Player
4+
from iblrig.test.base import TaskArgsMixin
5+
6+
7+
class TestPassiveVideo(TaskArgsMixin, unittest.TestCase):
8+
9+
def setUp(self):
10+
self.get_task_kwargs()
11+
12+
def test_next_trial(self):
13+
self.assertRaises(NotImplementedError, Session, **self.task_kwargs)
14+
self.task_kwargs['hardware_settings']['MAIN_SYNC'] = False
15+
task = Session(log_level='DEBUG', **self.task_kwargs)
16+
task.video = Mock(auto_spec=Player)
17+
task.task_params.VIDEO = r'C:\Users\Work\Downloads\ONE\perlin-xyscale2-tscale50-comb08-5min.mp4'
18+
task.task_params.VIDEO = r'C:\Users\Work\Downloads\SampleVideo_1280x720_1mb.mp4'
19+
task.next_trial()
20+
task.video.play.assert_called_once_with(task.task_params.VIDEO)
21+
task.video.replay.assert_not_called()
22+
task.video.reset_mock()
23+
task.next_trial()
24+
task.video.replay.assert_called_once()
25+
# task.bpod = MagicMock()
26+
# with patch.object(task, 'start_mixin_bpod'):
27+
# task.run()
28+
29+
30+
if __name__ == '__main__':
31+
unittest.main()

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "project_extraction"
7-
version = "0.4.2"
7+
version = "0.5.0"
88
description = "Custom extractors for satellite tasks"
99
dynamic = [ "readme" ]
1010
keywords = [ "IBL", "neuro-science" ]

0 commit comments

Comments
 (0)