From 16fa81d5afff7c530a68474ff5249f78333fa449 Mon Sep 17 00:00:00 2001 From: nadeemsadiq Date: Thu, 20 Jul 2023 19:41:09 +0100 Subject: [PATCH] feat: Intial POC of GoodGames Includes basic Video editing, OBS intergration, game detecting and clip editor --- .gitignore | 2 + ...uhu9uomolr.apps.googleusercontent.com.json | 1 + config.toml | 4 + main.py | 87 +++++ obs_recorder_and_game_dector.py | 124 ++++++++ video_editor.py | 299 ++++++++++++++++++ youtube_upload.py | 35 ++ 7 files changed, 552 insertions(+) create mode 100644 client_secret_6793728376-alv7pcqjm3psoibm3rd0cvuhu9uomolr.apps.googleusercontent.com.json create mode 100644 config.toml create mode 100644 main.py create mode 100644 obs_recorder_and_game_dector.py create mode 100644 video_editor.py create mode 100644 youtube_upload.py diff --git a/.gitignore b/.gitignore index f8b73e7..ec2968c 100644 --- a/.gitignore +++ b/.gitignore @@ -138,3 +138,5 @@ dmypy.json # Cython debug symbols cython_debug/ + +credentials.storage \ No newline at end of file diff --git a/client_secret_6793728376-alv7pcqjm3psoibm3rd0cvuhu9uomolr.apps.googleusercontent.com.json b/client_secret_6793728376-alv7pcqjm3psoibm3rd0cvuhu9uomolr.apps.googleusercontent.com.json new file mode 100644 index 0000000..062653d --- /dev/null +++ b/client_secret_6793728376-alv7pcqjm3psoibm3rd0cvuhu9uomolr.apps.googleusercontent.com.json @@ -0,0 +1 @@ +{"API_TOKEN_KEYS": ""} \ No newline at end of file diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..94306c4 --- /dev/null +++ b/config.toml @@ -0,0 +1,4 @@ +[connection] +host = "localhost" +port = 4455 +password = "" \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..8da9d7b --- /dev/null +++ b/main.py @@ -0,0 +1,87 @@ +import sys +from video_editor import MainWindow +from obs_recorder_and_game_dector import run_bg_app +from PySide6.QtCore import QStandardPaths, Qt, Slot, QThread, QTimer, QObject, Signal +from PySide6.QtGui import QAction, QIcon, QKeySequence, QScreen +from PySide6.QtWidgets import QApplication, QSystemTrayIcon, QMenu, QMainWindow, QLabel, QVBoxLayout, QWidget, QPushButton +from PySide6.QtMultimediaWidgets import QVideoWidget + +page_url = None + +class BackgroundThread(QThread): + def __init__(self,): + super().__init__() + def run(self): + global page_url + # Implement your background service logic here + while True: + new_video = run_bg_app() + + if new_video is not None: + self.sleep(3) + page_url = new_video + self.open_main_page.emit() + + self.sleep(1) # Sleep for 1 second + + # Define a signal for opening a new main page + open_main_page = Signal() + +class MainPage(QMainWindow): + def __init__(self): + super().__init__() + + self.setWindowTitle("Main Page") + self.resize(300, 200) + + # Create a label and a button + label = QLabel("Hello, World!") + button = QPushButton("Open New Page") + + # Create a layout and set it on a central widget + layout = QVBoxLayout() + layout.addWidget(label) + layout.addWidget(button) + + central_widget = QWidget() + central_widget.setLayout(layout) + self.setCentralWidget(central_widget) + self.new_page = None + + def open_new_page(self): + global page_url + self.new_page = MainWindow(page_url) + self.new_page.show() + + def closeEvent(self, event): + event.ignore() + +def main(): + app = QApplication(sys.argv) + + tray_icon = QSystemTrayIcon(app) + menu = QMenu() + + # Create the main page + main_page = MainPage() + + open_main_window = QAction("Open", app) + open_main_window.triggered.connect(main_page.open_new_page) + menu.addAction(open_main_window) + + exit_action = QAction("Exit", app) + exit_action.triggered.connect(app.quit) + menu.addAction(exit_action) + + tray_icon.setContextMenu(menu) + tray_icon.show() + + # Start the background thread + background_thread = BackgroundThread() + background_thread.open_main_page.connect(main_page.open_new_page) + background_thread.start() + + sys.exit(app.exec()) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/obs_recorder_and_game_dector.py b/obs_recorder_and_game_dector.py new file mode 100644 index 0000000..2f6e825 --- /dev/null +++ b/obs_recorder_and_game_dector.py @@ -0,0 +1,124 @@ +import obsws_python as obs +import time +import glob +import os +import sys +import subprocess + +subprocess.run("obs-studio &",shell=True) +time.sleep(5) +# pass conn info if not in config.toml +cl = obs.ReqClient(host='localhost', port=4455, password='AXB1hYYIeaCUQd9v', timeout=3) + + + +SOURCE_NAME = "..auto_game_dector_source" +SCENE_NAME = "..auto_game_dector_scene" +SOURCE_INPUT_TYPE = "xcomposite_input" +GAMES_WM_CLASS_NAME_TO_DETECT = ["steam_app_1304930", + "league of legends.exe", + "steam_app_582010", + "steam_app_739630", + "steam_app_438490", + "steam_app_321040", + "steam_app_820520"] + +class SceneStatus: + OK = 0 + MISSING_SCENE = 1 + SCENE_NOT_SELECTED = 2 + +class SourceStatus: + OK = 0 + MISSING_SOURCE = 1 + UNEXPECTED_SOURCES_FOUND = 2 + SOURCE_NOT_ENABLED = 3 + SOURCE_NOT_CREATED_WITH_DIFFERENT_VALUES = 4 + + +def verify_obs_setup(): + scene_status = verify_scene() + + if scene_status is SceneStatus.MISSING_SCENE: + init_scene() + + source_status = verify_source() + + if scene_status is SourceStatus.MISSING_SOURCE: + init_source() + +def verify_scene() -> SceneStatus: + get_scenes_response = cl.get_scene_list() + scenes = get_scenes_response.scenes + + for scene in scenes: + if scene['sceneName'] == SCENE_NAME: + if scene['sceneName'] == get_scenes_response.current_program_scene_name: + return SceneStatus.OK + else: + return SceneStatus.SCENE_NOT_SELECTED + + return SceneStatus.MISSING_SCENE + +def init_scene(): + cl.create_scene(name=SCENE_NAME) + cl.set_current_program_scene(name=SCENE_NAME) + +def verify_source() -> SourceStatus: + sources = cl.get_input_list().inputs + + for source in sources: + if source['inputName'] == SOURCE_NAME: + return SourceStatus.OK + + return SourceStatus.MISSING_SOURCE + +def init_source(): + cl.create_input(sceneName=SCENE_NAME,inputName=SOURCE_NAME,inputKind=SOURCE_INPUT_TYPE, inputSettings=None, sceneItemEnabled=True) + +def game_dector(): + windows = cl.get_input_properties_list_property_items(SOURCE_NAME,"capture_window").property_items + + for window in windows: + wmClassName = window['itemValue'].split("\r\n")[2] + + if wmClassName in GAMES_WM_CLASS_NAME_TO_DETECT: + return window['itemValue'] + + return None + + +verify_obs_setup() + +recording = cl.get_record_status().output_active + +def run_bg_app(): + global recording + global cl + + game_detected = game_dector() + + if game_detected is None: + if recording: + cl.stop_record() + + directory_path = cl.get_record_directory() + + list_of_files = glob.glob(directory_path.record_directory + "/*") # * means all if need specific format then *.csv + latest_file = max(list_of_files, key=os.path.getctime) + print(latest_file) + + recording = False + return latest_file + return None + + if not recording: + cl.set_input_settings(name=SOURCE_NAME, settings={'capture_window': game_detected}, overlay=True) + cl.start_record() + recording = True + return None + +if __name__ == '__main__': + while True: + run_bg_app() + time.sleep(1) \ No newline at end of file diff --git a/video_editor.py b/video_editor.py new file mode 100644 index 0000000..46728f2 --- /dev/null +++ b/video_editor.py @@ -0,0 +1,299 @@ +import sys +from PySide6.QtCore import QStandardPaths, Qt, Slot +from PySide6.QtGui import QAction, QIcon, QKeySequence, QScreen +from PySide6.QtWidgets import (QApplication, QDialog, QFileDialog, + QMainWindow, QSlider, QStyle, QToolBar, QPushButton) +from PySide6.QtMultimedia import (QAudio, QAudioOutput, QMediaFormat, + QMediaPlayer) +from PySide6.QtMultimediaWidgets import QVideoWidget +from moviepy.editor import * +from youtube_upload import upload_video +import uuid +import subprocess +import os + +AVI = "video/x-msvideo" # AVI + + +MP4 = 'video/mp4' + + +def get_supported_mime_types(): + result = [] + for f in QMediaFormat().supportedFileFormats(QMediaFormat.Decode): + mime_type = QMediaFormat(f).mimeType() + result.append(mime_type.name()) + return result + + +class MainWindow(QMainWindow): + + def __init__(self, fileUrl): + super().__init__() + + self._url = fileUrl + + self._playlist = [] # FIXME 6.3: Replace by QMediaPlaylist? + self._playlist_index = -1 + self._audio_output = QAudioOutput() + self._player = QMediaPlayer() + self._player.setAudioOutput(self._audio_output) + + self._player.errorOccurred.connect(self._player_error) + + tool_bar = QToolBar() + self.addToolBar(tool_bar) + + file_menu = self.menuBar().addMenu("&File") + icon = QIcon.fromTheme("document-open") + open_action = QAction(icon, "&Open...", self, + shortcut=QKeySequence.Open, triggered=self.open) + file_menu.addAction(open_action) + tool_bar.addAction(open_action) + icon = QIcon.fromTheme("application-exit") + exit_action = QAction(icon, "E&xit", self, + shortcut="Ctrl+Q", triggered=self.close) + file_menu.addAction(exit_action) + + play_menu = self.menuBar().addMenu("&Play") + style = self.style() + icon = QIcon.fromTheme("media-playback-start.png", + style.standardIcon(QStyle.SP_MediaPlay)) + self._play_action = tool_bar.addAction(icon, "Play") + self._play_action.triggered.connect(self._player.play) + play_menu.addAction(self._play_action) + + icon = QIcon.fromTheme("media-skip-backward-symbolic.svg", + style.standardIcon(QStyle.SP_MediaSkipBackward)) + self._previous_action = tool_bar.addAction(icon, "Previous") + self._previous_action.triggered.connect(self.previous_clicked) + play_menu.addAction(self._previous_action) + + icon = QIcon.fromTheme("media-playback-pause.png", + style.standardIcon(QStyle.SP_MediaPause)) + self._pause_action = tool_bar.addAction(icon, "Pause") + self._pause_action.triggered.connect(self._player.pause) + play_menu.addAction(self._pause_action) + + icon = QIcon.fromTheme("media-skip-forward-symbolic.svg", + style.standardIcon(QStyle.SP_MediaSkipForward)) + self._next_action = tool_bar.addAction(icon, "Next") + self._next_action.triggered.connect(self.next_clicked) + play_menu.addAction(self._next_action) + + icon = QIcon.fromTheme("media-playback-stop.png", + style.standardIcon(QStyle.SP_MediaStop)) + self._stop_action = tool_bar.addAction(icon, "Stop") + self._stop_action.triggered.connect(self._ensure_stopped) + play_menu.addAction(self._stop_action) + + self._scrub_slider = QSlider() + self._scrub_slider.setOrientation(Qt.Horizontal) + self._scrub_slider.setMinimum(0) + self._scrub_slider.setMaximum(100) + available_width = self.screen().availableGeometry().width() + self._scrub_slider.setFixedWidth(available_width / 8) + #self._scrub_slider.setValue(self._audio_output.volume()) + self._scrub_slider.setTickInterval(10) + #self._scrub_slider.setTickPosition(QSlider.TicksBelow) + self._scrub_slider.setToolTip("Scrub") + self._scrub_slider.sliderMoved.connect(self.set_position) + tool_bar.addWidget(self._scrub_slider) + + self._audio_output.setVolume(10) + self._volume_slider = QSlider() + self._volume_slider.setOrientation(Qt.Horizontal) + self._volume_slider.setMinimum(0) + self._volume_slider.setMaximum(100) + available_width = self.screen().availableGeometry().width() + self._volume_slider.setFixedWidth(available_width / 10) + self._volume_slider.setValue(self._audio_output.volume()) + self._volume_slider.setTickInterval(10) + self._volume_slider.setTickPosition(QSlider.TicksBelow) + self._volume_slider.setToolTip("Volume") + self._volume_slider.valueChanged.connect(self.volume_change) + tool_bar.addWidget(self._volume_slider) + + self._15SecondClip = QPushButton() + self._15SecondClip.setText("+ 15 Clip") + self._15SecondClip.setToolTip("+15 Second Clip") + self._15SecondClip.pressed.connect(self.clip_15_seconds) + tool_bar.addWidget(self._15SecondClip) + + self._30SecondClip = QPushButton() + self._30SecondClip.setText("+ 30 Clip") + self._30SecondClip.setToolTip("+30 Second Clip") + self._30SecondClip.pressed.connect(self.clip_30_seconds) + tool_bar.addWidget(self._30SecondClip) + + self._60SecondClip = QPushButton() + self._60SecondClip.setText("+ 60 Clip") + self._60SecondClip.setToolTip("+60 Second Clip") + self._60SecondClip.pressed.connect(self.clip_60_seconds) + tool_bar.addWidget(self._60SecondClip) + + self._upload = QPushButton() + self._upload.setText("YT") + self._upload.setToolTip("yt") + self._upload.pressed.connect(self.upload_video_youtube) + tool_bar.addWidget(self._upload) + + self._copy = QPushButton() + self._copy.setText("Copy") + self._copy.setToolTip("Copy") + self._copy.pressed.connect(self.copy) + tool_bar.addWidget(self._copy) + + about_menu = self.menuBar().addMenu("&About") + about_qt_act = QAction("About &Qt", self, triggered=qApp.aboutQt) + about_menu.addAction(about_qt_act) + + self._video_widget = QVideoWidget() + self.setCentralWidget(self._video_widget) + self._player.playbackStateChanged.connect(self.update_buttons) + self._player.setVideoOutput(self._video_widget) + + self.update_buttons(self._player.playbackState()) + self._mime_types = [] + + self._player.positionChanged.connect(self.position_changed) + self._player.durationChanged.connect(self.duration_changed) + + if self._url is not None: + self._playlist.append(self._url ) + self._playlist_index = len(self._playlist) - 1 + self._player.setSource(self._url) + self._player.play() + self._player.pause() + + def closeEvent(self, event): + self._ensure_stopped() + event.accept() + + def position_changed(self, position): + self._scrub_slider.setValue(position) + + def duration_changed(self, duration): + self._scrub_slider.setRange(0, duration) + + def set_position(self): + self._player.setPosition(self._scrub_slider.value()) + + def volume_change(self, position): + self._audio_output.setVolume(position / 100) + + def closeEvent(self, event): + event.ignore() + self._player.stop() + self.hide() + + @Slot() + def open(self): + self._ensure_stopped() + file_dialog = QFileDialog(self) + + is_windows = sys.platform == 'win32' + if not self._mime_types: + self._mime_types = get_supported_mime_types() + if (is_windows and AVI not in self._mime_types): + self._mime_types.append(AVI) + elif MP4 not in self._mime_types: + self._mime_types.append(MP4) + + file_dialog.setMimeTypeFilters(self._mime_types) + + default_mimetype = AVI if is_windows else MP4 + if default_mimetype in self._mime_types: + file_dialog.selectMimeTypeFilter(default_mimetype) + + movies_location = QStandardPaths.writableLocation(QStandardPaths.MoviesLocation) + file_dialog.setDirectory(movies_location) + if file_dialog.exec() == QDialog.Accepted: + self._url = file_dialog.selectedUrls()[0].toString() + self._playlist.append(self._url ) + self._playlist_index = len(self._playlist) - 1 + self._player.setSource(self._url) + self._player.play() + + @Slot() + def _ensure_stopped(self): + if self._player.playbackState() != QMediaPlayer.StoppedState: + self._player.stop() + + @Slot() + def previous_clicked(self): + # Go to previous track if we are within the first 5 seconds of playback + # Otherwise, seek to the beginning. + if self._player.position() <= 5000 and self._playlist_index > 0: + self._playlist_index -= 1 + self._playlist.previous() + self._player.setSource(self._playlist[self._playlist_index]) + else: + self._player.setPosition(0) + + @Slot() + def next_clicked(self): + if self._playlist_index < len(self._playlist) - 1: + self._playlist_index += 1 + self._player.setSource(self._playlist[self._playlist_index]) + + @Slot("QMediaPlayer::PlaybackState") + def update_buttons(self, state): + media_count = len(self._playlist) + self._play_action.setEnabled(media_count > 0 + and state != QMediaPlayer.PlayingState) + self._pause_action.setEnabled(state == QMediaPlayer.PlayingState) + self._stop_action.setEnabled(state != QMediaPlayer.StoppedState) + self._previous_action.setEnabled(self._player.position() > 0) + self._next_action.setEnabled(media_count > 1) + + def show_status_message(self, message): + self.statusBar().showMessage(message, 5000) + + @Slot("QMediaPlayer::Error", str) + def _player_error(self, error, error_string): + print(error_string, file=sys.stderr) + self.show_status_message(error_string) + + def clip_15_seconds(self): + self.clip_video(15) + + def clip_30_seconds(self): + self.clip_video(30) + + def clip_60_seconds(self): + self.clip_video(60) + + def clip_video(self, clip_length): + start_time = self._player.position() / 1000 + + dirname = os.path.dirname(__file__) + output_filepath = os.path.join(os.path.expanduser('~'), "Videos/gameplay/clips/"+str(uuid.uuid4())+".mp4") + + # Perform the saving operation with the selected file path + clip = VideoFileClip(self._url).subclip(start_time, start_time + clip_length) + clip.write_videofile(output_filepath) + clip.close() + + self.clip_window = MainWindow(output_filepath) + self.clip_window.show() + + def upload_video_youtube(self): + upload_video(self._url) + + def copy(self): + video_type = self._url.split(".")[-1] + subprocess.run("""echo "{url}" | xclip -sel clip -t text/uri-list -i""".format(video_type=video_type, url=self._url),shell=True) + + +def open_media_screen(): + app = QApplication(sys.argv) + main_win = MainWindow('~/Videos/gameplay/2023-07-02 21-17-35.mkv') + available_geometry = main_win.screen().availableGeometry() + main_win.resize(available_geometry.width() / 3, + available_geometry.height() / 2) + main_win.show() + sys.exit(app.exec()) + +if __name__ == "__main__": + open_media_screen() \ No newline at end of file diff --git a/youtube_upload.py b/youtube_upload.py new file mode 100644 index 0000000..19077fb --- /dev/null +++ b/youtube_upload.py @@ -0,0 +1,35 @@ +from simple_youtube_api.Channel import Channel +from simple_youtube_api.LocalVideo import LocalVideo + + +def upload_video(file_path): + # loggin into the channel + channel = Channel() + channel.login("client_secret_6793728376-alv7pcqjm3psoibm3rd0cvuhu9uomolr.apps.googleusercontent.com.json", "credentials.storage") + + # setting up the video that is going to be uploaded + video = LocalVideo(file_path=file_path) + + # setting snippet + video.set_title("Testing link sharing") + video.set_description("This is a description") + video.set_tags(["this", "tag"]) + video.set_category("gaming") + video.set_default_language("en-US") + + # setting status + video.set_embeddable(True) + video.set_license("creativeCommon") + video.set_privacy_status("private") + video.set_public_stats_viewable(True) + + # setting thumbnail + # video.set_thumbnail_path('86106086e9594672ad7408913b5a3a24.jpg') + + # uploading video and printing the results + video = channel.upload_video(video) + print(video.id) + print(video) + + # liking video + # video.like() \ No newline at end of file