From 0dbfeb0d9754325aa56ca95225c390f60b0e58ca Mon Sep 17 00:00:00 2001 From: Henk Reder Date: Tue, 3 Sep 2024 14:34:05 -0400 Subject: [PATCH 1/5] got rid of superfluous requirements --- requirements.txt | 165 +---------------------------------------------- 1 file changed, 2 insertions(+), 163 deletions(-) diff --git a/requirements.txt b/requirements.txt index 57c9c31..9b14b2c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,164 +1,3 @@ -certifi==2024.7.4 -charset-normalizer==3.3.2 -idna==3.8 -pyobjc==10.3.1 -pyobjc-core==10.3.1 -pyobjc-framework-Accessibility==10.3.1 -pyobjc-framework-Accounts==10.3.1 -pyobjc-framework-AddressBook==10.3.1 -pyobjc-framework-AdServices==10.3.1 -pyobjc-framework-AdSupport==10.3.1 -pyobjc-framework-AppleScriptKit==10.3.1 -pyobjc-framework-AppleScriptObjC==10.3.1 -pyobjc-framework-ApplicationServices==10.3.1 -pyobjc-framework-AppTrackingTransparency==10.3.1 -pyobjc-framework-AudioVideoBridging==10.3.1 -pyobjc-framework-AuthenticationServices==10.3.1 -pyobjc-framework-AutomaticAssessmentConfiguration==10.3.1 -pyobjc-framework-Automator==10.3.1 -pyobjc-framework-AVFoundation==10.3.1 -pyobjc-framework-AVKit==10.3.1 -pyobjc-framework-AVRouting==10.3.1 -pyobjc-framework-BackgroundAssets==10.3.1 -pyobjc-framework-BrowserEngineKit==10.3.1 -pyobjc-framework-BusinessChat==10.3.1 -pyobjc-framework-CalendarStore==10.3.1 -pyobjc-framework-CallKit==10.3.1 -pyobjc-framework-CFNetwork==10.3.1 -pyobjc-framework-Cinematic==10.3.1 -pyobjc-framework-ClassKit==10.3.1 -pyobjc-framework-CloudKit==10.3.1 -pyobjc-framework-Cocoa==10.3.1 -pyobjc-framework-Collaboration==10.3.1 -pyobjc-framework-ColorSync==10.3.1 -pyobjc-framework-Contacts==10.3.1 -pyobjc-framework-ContactsUI==10.3.1 -pyobjc-framework-CoreAudio==10.3.1 -pyobjc-framework-CoreAudioKit==10.3.1 -pyobjc-framework-CoreBluetooth==10.3.1 -pyobjc-framework-CoreData==10.3.1 -pyobjc-framework-CoreHaptics==10.3.1 -pyobjc-framework-CoreLocation==10.3.1 -pyobjc-framework-CoreMedia==10.3.1 -pyobjc-framework-CoreMediaIO==10.3.1 -pyobjc-framework-CoreMIDI==10.3.1 -pyobjc-framework-CoreML==10.3.1 -pyobjc-framework-CoreMotion==10.3.1 -pyobjc-framework-CoreServices==10.3.1 -pyobjc-framework-CoreSpotlight==10.3.1 -pyobjc-framework-CoreText==10.3.1 -pyobjc-framework-CoreWLAN==10.3.1 -pyobjc-framework-CryptoTokenKit==10.3.1 -pyobjc-framework-DataDetection==10.3.1 -pyobjc-framework-DeviceCheck==10.3.1 -pyobjc-framework-DictionaryServices==10.3.1 -pyobjc-framework-DiscRecording==10.3.1 -pyobjc-framework-DiscRecordingUI==10.3.1 -pyobjc-framework-DiskArbitration==10.3.1 -pyobjc-framework-DVDPlayback==10.3.1 -pyobjc-framework-EventKit==10.3.1 -pyobjc-framework-ExceptionHandling==10.3.1 -pyobjc-framework-ExecutionPolicy==10.3.1 -pyobjc-framework-ExtensionKit==10.3.1 -pyobjc-framework-ExternalAccessory==10.3.1 -pyobjc-framework-FileProvider==10.3.1 -pyobjc-framework-FileProviderUI==10.3.1 -pyobjc-framework-FinderSync==10.3.1 -pyobjc-framework-FSEvents==10.3.1 -pyobjc-framework-GameCenter==10.3.1 -pyobjc-framework-GameController==10.3.1 -pyobjc-framework-GameKit==10.3.1 -pyobjc-framework-GameplayKit==10.3.1 -pyobjc-framework-HealthKit==10.3.1 -pyobjc-framework-ImageCaptureCore==10.3.1 -pyobjc-framework-InputMethodKit==10.3.1 -pyobjc-framework-InstallerPlugins==10.3.1 -pyobjc-framework-InstantMessage==10.3.1 -pyobjc-framework-Intents==10.3.1 -pyobjc-framework-IntentsUI==10.3.1 -pyobjc-framework-IOBluetooth==10.3.1 -pyobjc-framework-IOBluetoothUI==10.3.1 -pyobjc-framework-IOSurface==10.3.1 -pyobjc-framework-iTunesLibrary==10.3.1 -pyobjc-framework-KernelManagement==10.3.1 -pyobjc-framework-LatentSemanticMapping==10.3.1 -pyobjc-framework-LaunchServices==10.3.1 -pyobjc-framework-libdispatch==10.3.1 -pyobjc-framework-libxpc==10.3.1 -pyobjc-framework-LinkPresentation==10.3.1 -pyobjc-framework-LocalAuthentication==10.3.1 -pyobjc-framework-LocalAuthenticationEmbeddedUI==10.3.1 -pyobjc-framework-MailKit==10.3.1 -pyobjc-framework-MapKit==10.3.1 -pyobjc-framework-MediaAccessibility==10.3.1 -pyobjc-framework-MediaLibrary==10.3.1 -pyobjc-framework-MediaPlayer==10.3.1 -pyobjc-framework-MediaToolbox==10.3.1 -pyobjc-framework-Metal==10.3.1 -pyobjc-framework-MetalFX==10.3.1 -pyobjc-framework-MetalKit==10.3.1 -pyobjc-framework-MetalPerformanceShaders==10.3.1 -pyobjc-framework-MetalPerformanceShadersGraph==10.3.1 -pyobjc-framework-MetricKit==10.3.1 -pyobjc-framework-MLCompute==10.3.1 -pyobjc-framework-ModelIO==10.3.1 -pyobjc-framework-MultipeerConnectivity==10.3.1 -pyobjc-framework-NaturalLanguage==10.3.1 -pyobjc-framework-NetFS==10.3.1 -pyobjc-framework-Network==10.3.1 -pyobjc-framework-NetworkExtension==10.3.1 -pyobjc-framework-NotificationCenter==10.3.1 -pyobjc-framework-OpenDirectory==10.3.1 -pyobjc-framework-OSAKit==10.3.1 -pyobjc-framework-OSLog==10.3.1 -pyobjc-framework-PassKit==10.3.1 -pyobjc-framework-PencilKit==10.3.1 -pyobjc-framework-PHASE==10.3.1 -pyobjc-framework-Photos==10.3.1 -pyobjc-framework-PhotosUI==10.3.1 -pyobjc-framework-PreferencePanes==10.3.1 -pyobjc-framework-PushKit==10.3.1 -pyobjc-framework-Quartz==10.3.1 -pyobjc-framework-QuickLookThumbnailing==10.3.1 -pyobjc-framework-ReplayKit==10.3.1 -pyobjc-framework-SafariServices==10.3.1 -pyobjc-framework-SafetyKit==10.3.1 -pyobjc-framework-SceneKit==10.3.1 -pyobjc-framework-ScreenCaptureKit==10.3.1 -pyobjc-framework-ScreenSaver==10.3.1 -pyobjc-framework-ScreenTime==10.3.1 -pyobjc-framework-ScriptingBridge==10.3.1 -pyobjc-framework-SearchKit==10.3.1 -pyobjc-framework-Security==10.3.1 -pyobjc-framework-SecurityFoundation==10.3.1 -pyobjc-framework-SecurityInterface==10.3.1 -pyobjc-framework-SensitiveContentAnalysis==10.3.1 -pyobjc-framework-ServiceManagement==10.3.1 -pyobjc-framework-SharedWithYou==10.3.1 -pyobjc-framework-SharedWithYouCore==10.3.1 -pyobjc-framework-ShazamKit==10.3.1 -pyobjc-framework-Social==10.3.1 -pyobjc-framework-SoundAnalysis==10.3.1 -pyobjc-framework-Speech==10.3.1 -pyobjc-framework-SpriteKit==10.3.1 -pyobjc-framework-StoreKit==10.3.1 -pyobjc-framework-Symbols==10.3.1 -pyobjc-framework-SyncServices==10.3.1 -pyobjc-framework-SystemConfiguration==10.3.1 -pyobjc-framework-SystemExtensions==10.3.1 -pyobjc-framework-ThreadNetwork==10.3.1 -pyobjc-framework-UniformTypeIdentifiers==10.3.1 -pyobjc-framework-UserNotifications==10.3.1 -pyobjc-framework-UserNotificationsUI==10.3.1 -pyobjc-framework-VideoSubscriberAccount==10.3.1 -pyobjc-framework-VideoToolbox==10.3.1 -pyobjc-framework-Virtualization==10.3.1 -pyobjc-framework-Vision==10.3.1 -pyobjc-framework-WebKit==10.3.1 PySide6==6.7.2 -PySide6_Addons==6.7.2 -PySide6_Essentials==6.7.2 -python-vlc==3.0.20123 -requests==2.32.3 -shiboken6==6.7.2 -urllib3==2.2.2 +Requests==2.32.3 +python_vlc==3.0.20123 From 01683fb5038441414b94db9efd46c0825294fc80 Mon Sep 17 00:00:00 2001 From: Henk Reder Date: Thu, 5 Sep 2024 10:46:00 -0400 Subject: [PATCH 2/5] rewrote API to use RC interface --- xapi_vlc/main.py | 64 ++++---------------------- xapi_vlc/vlc/__init__.py | 0 xapi_vlc/vlc/api.py | 99 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 56 deletions(-) create mode 100644 xapi_vlc/vlc/__init__.py create mode 100644 xapi_vlc/vlc/api.py diff --git a/xapi_vlc/main.py b/xapi_vlc/main.py index 9cb7cad..531e716 100644 --- a/xapi_vlc/main.py +++ b/xapi_vlc/main.py @@ -1,63 +1,15 @@ import sys -import vlc import argparse +from xapi_vlc.vlc.api import VLCController from xapi_vlc.xapi import client from xapi_vlc.xapi import statement -from PySide6.QtWidgets import QApplication, QMainWindow -from PySide6.QtCore import Qt - -def make_client(player, userid): - +def make_client(userid): def client_fn(event): - data = statement.create(event, player, userid) + data = statement.create(event, userid) client.send(data) - return client_fn -class VLCWindow(QMainWindow): - def __init__(self, filepath=None, userid=None): - super().__init__() - - # Set up VLC player - self.instance = vlc.Instance() - self.player = self.instance.media_player_new() - media = self.instance.media_new(filepath) - self.player.set_media(media) - self.player.set_nsobject(self.winId()) - - # Attach Events - # logger = make_logger(self.player) - client = make_client(self.player, userid) - event_manager = self.player.event_manager() - event_manager.event_attach(vlc.EventType.MediaPlayerPlaying, client) - event_manager.event_attach(vlc.EventType.MediaPlayerEndReached, client) - event_manager.event_attach(vlc.EventType.MediaPlayerPaused, client) - - # Start playing the media - self.player.play() - - # Set the window title and size - self.setWindowTitle("VLC Player") - self.setGeometry(100, 100, 800, 600) - - def keyPressEvent(self, event): - """Handle key press events.""" - if event.key() == Qt.Key_Space: - self.toggle_play_pause() - - def toggle_play_pause(self): - """Toggle play/pause of the VLC player.""" - if self.player.is_playing(): - self.player.pause() - else: - self.player.play() - - def closeEvent(self, event): - """Handle window close event.""" - self.player.stop() - event.accept() - def main(): parser = argparse.ArgumentParser(description="VLC Player that sends statements to an LRS.") parser.add_argument('--content', type=str, required=True, help="Path to content to be played.") @@ -65,11 +17,11 @@ def main(): args = parser.parse_args() - - app = QApplication(sys.argv) - window = VLCWindow(filepath=args.content, userid=args.userid) - window.show() - sys.exit(app.exec()) + client = make_client(args.userid) + controller = VLCController(filepath=args.content) + controller.play() + print("Starting Event Watcher...") + controller.start_event_watcher(client) if __name__ == "__main__": main() diff --git a/xapi_vlc/vlc/__init__.py b/xapi_vlc/vlc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/xapi_vlc/vlc/api.py b/xapi_vlc/vlc/api.py new file mode 100644 index 0000000..6802363 --- /dev/null +++ b/xapi_vlc/vlc/api.py @@ -0,0 +1,99 @@ +import sys +import subprocess +import time +import pexpect +import threading +import atexit +import re + +# function that strips the data of the command and only returns response. +def parse_response(output): + match = re.search(r'^\w+\r?\n([\s\S]*)', output) + if match: + return match.group(1) + return None + +class VLCController: + def __init__(self, filepath=None): + self.vlc_process = pexpect.spawn(f"vlc --extraintf rc {filepath}") + self.previous_state = None + time.sleep(2) # Allow time for VLC to start + self.running = False + self.event_thread = None + + # calls stop event watcher when exit occurs + atexit.register(self.stop_event_watcher) + + ## communications and parsing RC + def flush_output(self): + """Flush any leftover output from the previous command.""" + while self.vlc_process.expect(['>', pexpect.TIMEOUT], timeout=0.1) == 0: + self.vlc_process.before + + def send_command(self, command): + self.flush_output() + self.vlc_process.sendline(command) + self.vlc_process.expect('>') + return parse_response(self.vlc_process.before.decode('utf-8').strip()) + + def get_time(self): + response = self.send_command("get_time") + try: + return int(response) + except TypeError: + return 0 + + def get_title(self): + response = self.send_command("get_title") + return response + + def get_status(self): + return self.send_command("status") + + def get_length(self): + response = self.send_command("get_length") + try: + return int(response) + except TypeError: + return 0 + + def form_metadata_map(self, status): + return {"status": status, + "title": self.get_title(), + "time": self.get_time(), + "length": self.get_length()} + + def play(self): + self.send_command("play") + + + ## Event Watching + def parse_status(self, status_output, callback): + if "state playing" in status_output and self.previous_state != "playing": + callback(self.form_metadata_map("playing")) + self.previous_state = "playing" + elif "state paused" in status_output and self.previous_state != "paused": + callback(self.form_metadata_map("paused")) + self.previous_state = "paused" + elif "state stopped" in status_output and self.previous_state != "stopped": + callback(self.form_metadata_map("stopped")) + self.previous_state = "stopped" + elif "state ended" in status_output and self.previous_state != "ended": + callback(self.form_metadata_map("finished")) + self.previous_state = "ended" + + def on_state_change(self, callback): + while self.running: + status = self.get_status() + self.parse_status(status, callback) + + def start_event_watcher(self, callback): + if not self.running: + self.running = True + self.event_thread = threading.Thread(target=self.on_state_change, args=(callback,)) + self.event_thread.start() + + def stop_event_watcher(self): + self.running = False + if self.event_thread: + self.event_thread.join() From b8af71976db6b16c21d2ada2ac539b8b93f0912d Mon Sep 17 00:00:00 2001 From: Henk Reder Date: Thu, 5 Sep 2024 10:46:49 -0400 Subject: [PATCH 3/5] changed env var to DOMAIN instead of ENDPOINT --- README.md | 2 +- xapi_vlc/xapi/client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a1e6b8c..41ff219 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ The dependencies will be made available in the `env` directory. 3) Set the following env vars: ``` -VLC_LRS_ENDPOINT= +VLC_LRS_DOMAIN= VLC_LRS_KEY= VLC_LRS_SECRET= ``` diff --git a/xapi_vlc/xapi/client.py b/xapi_vlc/xapi/client.py index cf28635..049903a 100644 --- a/xapi_vlc/xapi/client.py +++ b/xapi_vlc/xapi/client.py @@ -6,7 +6,7 @@ def send(data): # get creds go here lrs_key = os.environ.get('VLC_LRS_KEY') lrs_secret = os.environ.get('VLC_LRS_SECRET') - url = os.environ.get('VLC_LRS_ENDPOINT') + url = os.environ.get('VLC_LRS_DOMAIN') headers = { "Content-Type": "application/json", From de1ec1a84ec9f4005142be87b400fdd275e61a21 Mon Sep 17 00:00:00 2001 From: Henk Reder Date: Thu, 5 Sep 2024 10:47:46 -0400 Subject: [PATCH 4/5] changed statement former to account for new api --- xapi_vlc/xapi/statement.py | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/xapi_vlc/xapi/statement.py b/xapi_vlc/xapi/statement.py index c6af146..2fd5f94 100644 --- a/xapi_vlc/xapi/statement.py +++ b/xapi_vlc/xapi/statement.py @@ -1,35 +1,24 @@ from datetime import datetime -def create(event, player, userid): +def create(event, userid): base_url = "https://yet.systems/xapi/profiles/vlc" verb_url = base_url+"/verbs" activity_url = base_url+"/activitytype" video_url = activity_url+"/video" # TODO: integrate playback time into the statements - playback_time = player.get_time() - playback_time_seconds = playback_time / 1000.0 - print(f"Event type: {event.type} occurred at {playback_time_seconds:.2f} seconds") - - media = player.get_media() - - # TODO: integrate metadata into statements: http://www.olivieraubert.net/vlc/python-ctypes/doc/vlc.Meta-class.html - title = media.get_meta(vlc.Meta.Title) - director = media.get_media(vlc.Meta.Director) - url = media.get_media(vlc.Meta.URL) - print(f"Title: {title}") - print(f"Director: {director}") - print(f"URL: {url}") + time = event['time'] + length = event['length'] return { "actor": { "mbox": f"mailto:{userid}" }, "verb": { - "id": f"{verb_url}/{event.type}" + "id": f"{verb_url}/{event['status']}" }, "object": { - "id": f"{video_url}/{media.get_mrl()}" + "id": f"{video_url}/{event['title']}" # "definition": { # "name": {"en-US": media_title}, # "description": {"en-US": f"The media file being {verb['display']['en-US']}."} From 2c7f3b58fc8fbc349755a842722af5a748473bb7 Mon Sep 17 00:00:00 2001 From: Henk Reder Date: Thu, 5 Sep 2024 10:48:26 -0400 Subject: [PATCH 5/5] got rid of uneeded deps --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 9b14b2c..d8ba430 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ -PySide6==6.7.2 +pexpect==4.9.0 Requests==2.32.3 -python_vlc==3.0.20123