-
-
Notifications
You must be signed in to change notification settings - Fork 189
feat: add capture.py - also fixes audio recording #362
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
71 commits
Select commit
Hold shift + click to select a range
7ea743d
merge
0dm c707f03
Create capture.py
0dm f5cb9bf
Update capture.py
0dm fad652d
Update capture.py
0dm 5d41eba
it's finally fixed
0dm 2648581
add dependencies
0dm b1aa358
comment
0dm 6e4163e
move code + use config.CAPTURE_DIR_PATH
0dm ac6b02b
remove debug lines
0dm ac63470
Update capture.py
0dm fb84ad9
OpenAdaptCapture -> Capture
0dm e45dfa6
add camera
0dm be285d4
Let's have this off by default.
0dm 5c14ab5
Merge remote-tracking branch 'upstream/main'
0dm 6620f53
Merge remote-tracking branch 'upstream/main' into macos-capture
0dm 709ff38
Merge remote-tracking branch 'upstream/main' into macos-capture
0dm 40db7ab
Merge remote-tracking branch 'upstream/main'
0dm e85444c
Merge remote-tracking branch 'upstream/main'
0dm 7484446
hotfix
0dm 9a67c71
fix
0dm d3ac547
Merge remote-tracking branch 'upstream/main' into macos-capture
0dm 949c018
Merge remote-tracking branch 'upstream/main' into macos-capture
0dm aa704be
linting
0dm 857fb1c
merge
0dm 349a724
Create capture.py
0dm c3011a4
windows
0dm f2b19e2
Merge remote-tracking branch 'upstream/main' into macos-capture
0dm 7a0b7ae
cleanup + lint
0dm 8b76a6b
Merge branch 'main' into macos-capture
0dm c876b43
Merge branch 'main' into macos-capture
0dm 6ca4ebc
Merge remote-tracking branch 'upstream/main' into macos-capture
0dm ee6efcb
Update _windows.py
0dm 186914d
Merge branch 'macos-capture' of https://github.com/0dm/OpenAdapt into…
0dm 030ffb4
Merge remote-tracking branch 'upstream/main'
0dm 87dda56
add audio + new windows recording
0dm 904867a
screen_recorder.free_resources()
0dm b4fea77
Update _windows.py
0dm 528e501
isort
0dm f73697e
Merge remote-tracking branch 'upstream/main' into macos-capture
0dm 5f60828
Merge remote-tracking branch 'upstream/main'
0dm 13307d1
Merge remote-tracking branch 'upstream/main' into macos-capture
0dm 92ff5a5
Merge remote-tracking branch 'upstream/main' into macos-capture
0dm c4e668a
Merge remote-tracking branch 'upstream/main'
0dm 02f48c8
Merge remote-tracking branch 'upstream/main' into macos-capture
0dm 5dc6735
add playback recording
0dm 3d2dc53
Update replay.py
0dm 78f1a25
Update replay.py
0dm 27ce545
Merge remote-tracking branch 'upstream/main' into macos-capture
0dm a98cf3b
Merge remote-tracking branch 'upstream/main' into macos-capture
0dm 2be979a
Merge remote-tracking branch 'upstream/main'
0dm 7064103
Update README.md
0dm 0fe8156
Revert "Update README.md"
0dm f3e432a
Update README.md
0dm e17e795
Revert "Revert "Update README.md""
0dm f0b161e
Merge branch 'main' of https://github.com/0dm/OpenAdapt
0dm 278a108
Update README.md
0dm d79946b
Merge remote-tracking branch 'upstream/main' into macos-capture
0dm 5801cef
Merge remote-tracking branch 'upstream/main' into macos-capture
0dm 90ba6f3
run pre-commit
0dm 7e7f068
Merge remote-tracking branch 'upstream/main' into macos-capture
0dm ee1b2fe
Update pyproject.toml
0dm ad78215
Update openadapt/replay.py
abrichr 5ca69c6
Update openadapt/replay.py
abrichr 6caa478
Merge remote-tracking branch 'upstream/main' into macos-capture
0dm a8a59aa
Update openadapt/replay.py
abrichr 628b5b6
Merge branch 'macos-capture' of https://github.com/0dm/OpenAdapt into…
0dm c399b5a
Update replay.py
0dm ae46894
update poetry.lock
abrichr 270b1e0
merge main
abrichr 2326c13
merge to main and lock
abrichr ca8f8c4
Merge branch 'main' into macos-capture
abrichr File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
"""Capture the screen, audio, and camera as a video on macOS and Windows. | ||
|
||
Module: capture.py | ||
""" | ||
import sys | ||
|
||
if sys.platform == "darwin": | ||
from . import _macos as impl | ||
elif sys.platform == "win32": | ||
from . import _windows as impl | ||
else: | ||
raise Exception(f"Unsupported platform: {sys.platform}") | ||
|
||
device = impl.Capture() | ||
|
||
|
||
def get_capture() -> impl.Capture: | ||
"""Get the capture object. | ||
|
||
Returns: | ||
Capture: The capture object. | ||
""" | ||
return device | ||
|
||
|
||
def start(audio: bool = False, camera: bool = False) -> None: | ||
"""Start the capture.""" | ||
device.start(audio=audio, camera=camera) | ||
|
||
|
||
def stop() -> None: | ||
"""Stop the capture.""" | ||
device.stop() | ||
|
||
|
||
def test() -> None: | ||
"""Test the capture.""" | ||
device.start() | ||
input("Press enter to stop") | ||
device.stop() | ||
|
||
|
||
if __name__ in ("__main__", "capture"): | ||
test() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
"""Allows for capturing the screen and audio on macOS. | ||
|
||
This is based on: https://gist.github.com/timsutton/0c6439eb6eb1621a5964 | ||
|
||
usage: see bottom of file | ||
""" | ||
from datetime import datetime | ||
from sys import platform | ||
import os | ||
|
||
from Foundation import NSURL, NSObject # type: ignore # noqa | ||
from Quartz import CGMainDisplayID # type: ignore # noqa | ||
import AVFoundation as AVF # type: ignore # noqa | ||
import objc # type: ignore # noqa | ||
|
||
from openadapt import config | ||
|
||
|
||
class Capture: | ||
"""Capture the screen, audio, and camera on macOS.""" | ||
|
||
def __init__(self) -> None: | ||
"""Initialize the capture object.""" | ||
if platform != "darwin": | ||
raise NotImplementedError( | ||
"This is the macOS implementation, please use the Windows version" | ||
) | ||
|
||
objc.options.structs_indexable = True | ||
|
||
def start(self, audio: bool = False, camera: bool = False) -> None: | ||
"""Start capturing the screen, audio, and camera. | ||
|
||
Args: | ||
audio (bool, optional): Whether to capture audio (default: False). | ||
camera (bool, optional): Whether to capture the camera (default: False). | ||
""" | ||
self.display_id = CGMainDisplayID() | ||
self.session = AVF.AVCaptureSession.alloc().init() | ||
self.screen_input = AVF.AVCaptureScreenInput.alloc().initWithDisplayID_( | ||
self.display_id | ||
) | ||
self.file_output = AVF.AVCaptureMovieFileOutput.alloc().init() | ||
self.camera_session = None # not used if camera=False | ||
|
||
# Create an audio device input with the default audio device | ||
self.audio_input = AVF.AVCaptureDeviceInput.alloc().initWithDevice_error_( | ||
AVF.AVCaptureDevice.defaultDeviceWithMediaType_(AVF.AVMediaTypeAudio), None | ||
) | ||
|
||
if not os.path.exists(config.CAPTURE_DIR_PATH): | ||
os.mkdir(config.CAPTURE_DIR_PATH) | ||
self.file_url = NSURL.fileURLWithPath_( | ||
os.path.join( | ||
config.CAPTURE_DIR_PATH, | ||
datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + ".mov", | ||
) | ||
) | ||
if audio and self.session.canAddInput_(self.audio_input[0]): | ||
self.session.addInput_(self.audio_input[0]) | ||
|
||
if self.session.canAddInput_(self.screen_input): | ||
self.session.addInput_(self.screen_input) | ||
|
||
self.session.addOutput_(self.file_output) | ||
|
||
self.session.startRunning() | ||
|
||
# Cheat and pass a dummy delegate object where | ||
# normally we'd have a AVCaptureFileOutputRecordingDelegate | ||
self.file_url = ( | ||
self.file_output.startRecordingToOutputFileURL_recordingDelegate_( | ||
self.file_url, NSObject.alloc().init() | ||
) | ||
) | ||
|
||
if camera: | ||
self._use_camera() | ||
|
||
def _use_camera(self) -> None: | ||
"""Start capturing the camera.""" | ||
self.camera_session = AVF.AVCaptureSession.alloc().init() | ||
self.camera_file_output = AVF.AVCaptureMovieFileOutput.alloc().init() | ||
self.camera_input = AVF.AVCaptureDeviceInput.alloc().initWithDevice_error_( | ||
AVF.AVCaptureDevice.defaultDeviceWithMediaType_(AVF.AVMediaTypeVideo), None | ||
) | ||
|
||
if self.camera_session.canAddInput_(self.camera_input[0]): | ||
self.camera_session.addInput_(self.camera_input[0]) | ||
self.camera_session.startRunning() | ||
|
||
self.camera_session.addOutput_(self.camera_file_output) | ||
|
||
self.camera_url = ( | ||
self.camera_file_output.startRecordingToOutputFileURL_recordingDelegate_( | ||
NSURL.fileURLWithPath_( | ||
os.path.join( | ||
config.CAPTURE_DIR_PATH, | ||
datetime.now().strftime("camera.%Y-%m-%d-%H-%M-%S") + ".mov", | ||
) | ||
), | ||
NSObject.alloc().init(), | ||
) | ||
) | ||
|
||
def stop(self) -> None: | ||
"""Stop capturing the screen, audio, and camera.""" | ||
self.session.stopRunning() | ||
if self.camera_session: | ||
self.camera_session.stopRunning() | ||
|
||
|
||
if __name__ == "__main__": | ||
capture = Capture() | ||
capture.start(audio=True, camera=False) | ||
input("Press enter to stop") | ||
capture.stop() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
"""Allows for capturing the screen and audio on Windows.""" | ||
from datetime import datetime | ||
from sys import platform | ||
import os | ||
import wave | ||
|
||
from screen_recorder_sdk import screen_recorder | ||
import pyaudio | ||
|
||
from openadapt import config | ||
|
||
|
||
class Capture: | ||
"""Capture the screen video and audio on Windows.""" | ||
|
||
def __init__(self, pid: int = 0) -> None: | ||
"""Initialize the capture object. | ||
|
||
Args: | ||
pid (int, optional): The process ID of the window to capture. | ||
Defaults to 0 (the entire screen) | ||
""" | ||
if platform != "win32": | ||
raise NotImplementedError( | ||
"This is the Windows implementation, please use the macOS version" | ||
) | ||
self.is_recording = False | ||
self.video_out = None | ||
self.audio_out = None | ||
self.pid = pid | ||
|
||
screen_recorder.init_resources(screen_recorder.RecorderParams(pid=self.pid)) | ||
|
||
# Initialize PyAudio | ||
self.audio = pyaudio.PyAudio() | ||
self.audio_stream = None | ||
self.audio_frames = [] | ||
|
||
def start(self, audio: bool = True) -> None: | ||
"""Start capturing the screen video and audio. | ||
|
||
Args: | ||
audio (bool): Whether to capture audio. | ||
""" | ||
if self.is_recording: | ||
raise RuntimeError("Recording is already in progress") | ||
self.is_recording = True | ||
|
||
# Start video recording | ||
self.video_out = os.path.join( | ||
config.CAPTURES_DIR, | ||
datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + ".mov", | ||
) | ||
screen_recorder.start_video_recording(self.video_out, 30, 8000000, True) | ||
|
||
# Start audio recording | ||
if audio: | ||
self.audio_out = os.path.join( | ||
config.CAPTURES_DIR, | ||
datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + ".wav", | ||
) | ||
self.audio_stream = self.audio.open( | ||
format=pyaudio.paInt16, | ||
channels=2, | ||
rate=44100, | ||
input=True, | ||
frames_per_buffer=1024, | ||
stream_callback=self._audio_callback, | ||
) | ||
self.audio_frames = [] | ||
|
||
def _audio_callback( | ||
self, in_data: bytes, frame_count: int, time_info: dict, status: int | ||
) -> tuple: | ||
self.audio_frames.append(in_data) | ||
return (None, pyaudio.paContinue) | ||
|
||
def stop(self) -> None: | ||
"""Stop capturing the screen video and audio.""" | ||
if self.is_recording: | ||
screen_recorder.stop_video_recording() | ||
if self.audio_stream: | ||
self.audio_stream.stop_stream() | ||
self.audio_stream.close() | ||
self.audio.terminate() | ||
self.save_audio() | ||
self.is_recording = False | ||
screen_recorder.free_resources() | ||
|
||
def save_audio(self) -> None: | ||
"""Save the captured audio to a WAV file.""" | ||
with wave.open(self.audio_out, "wb") as wf: | ||
wf.setnchannels(2) | ||
wf.setsampwidth(self.audio.get_sample_size(pyaudio.paInt16)) | ||
wf.setframerate(44100) | ||
wf.writeframes(b"".join(self.audio_frames)) | ||
|
||
|
||
if __name__ == "__main__": | ||
capture = Capture() | ||
capture.start() | ||
input("Press enter to stop") | ||
capture.stop() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.