rbx 3.18.3__tar.gz → 3.18.3.dev157__tar.gz
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.
- {rbx-3.18.3 → rbx-3.18.3.dev157}/PKG-INFO +15 -16
- {rbx-3.18.3 → rbx-3.18.3.dev157}/README.md +1 -1
- {rbx-3.18.3 → rbx-3.18.3.dev157}/pyproject.toml +16 -17
- rbx-3.18.3.dev157/rbx/__init__.py +1 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/toolkit/__init__.py +2 -43
- rbx-3.18.3.dev157/rbx/toolkit/browser.py +195 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/toolkit/media.py +1 -9
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx.egg-info/PKG-INFO +15 -16
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx.egg-info/SOURCES.txt +0 -1
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx.egg-info/requires.txt +14 -15
- rbx-3.18.3/rbx/__init__.py +0 -1
- rbx-3.18.3/rbx/toolkit/browser.py +0 -141
- rbx-3.18.3/rbx/toolkit/web.py +0 -8
- {rbx-3.18.3 → rbx-3.18.3.dev157}/LICENSE +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/auth/__init__.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/auth/decorators.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/auth/keystore.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/auth/mock.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/aws/__init__.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/aws/s3.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/buildtools/__init__.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/buildtools/cli.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/buildtools/tasks/__init__.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/buildtools/tasks/apprunner.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/buildtools/tasks/ec2.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/buildtools/tasks/image.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/buildtools/tasks/misc.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/clients/__init__.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/clients/adsquare.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/clients/broadsign.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/clients/oxr.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/clients/panels.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/clients/reporting.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/clients/retry.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/exceptions.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/gcp/__init__.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/gcp/cloud_tasks.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/gcp/pubsub.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/gcp/storage.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/logging.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/settings.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/toolkit/cli.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/toolkit/exporter.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/toolkit/utils.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/utils/__init__.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/utils/mdm.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/utils/vast.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/web/__init__.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx/web/handlers.py +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx.egg-info/dependency_links.txt +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx.egg-info/entry_points.txt +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/rbx.egg-info/top_level.txt +0 -0
- {rbx-3.18.3 → rbx-3.18.3.dev157}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rbx
|
|
3
|
-
Version: 3.18.3
|
|
3
|
+
Version: 3.18.3.dev157
|
|
4
4
|
Summary: A collection of common tools for Scoota services.
|
|
5
5
|
Author-email: The Scoota Engineering Team <engineering@scoota.com>
|
|
6
6
|
License:
|
|
@@ -228,6 +228,19 @@ Requires-Dist: Click<9
|
|
|
228
228
|
Requires-Dist: colorama
|
|
229
229
|
Requires-Dist: lxml<6,>=5.3.0
|
|
230
230
|
Requires-Dist: requests>=2.31.0
|
|
231
|
+
Provides-Extra: all
|
|
232
|
+
Requires-Dist: boto3; extra == "all"
|
|
233
|
+
Requires-Dist: fabric~=3.2.0; extra == "all"
|
|
234
|
+
Requires-Dist: google-cloud-error-reporting<2,>=1.9.2; extra == "all"
|
|
235
|
+
Requires-Dist: google-cloud-firestore<3,>=2.3; extra == "all"
|
|
236
|
+
Requires-Dist: google-cloud-pubsub<3,>=2.9; extra == "all"
|
|
237
|
+
Requires-Dist: google-cloud-storage<3,>=2.1; extra == "all"
|
|
238
|
+
Requires-Dist: google-cloud-tasks<3,>=2.7; extra == "all"
|
|
239
|
+
Requires-Dist: Jinja2==3.1.2; extra == "all"
|
|
240
|
+
Requires-Dist: playwright~=1.51.0; extra == "all"
|
|
241
|
+
Requires-Dist: sh~=2.0.6; extra == "all"
|
|
242
|
+
Requires-Dist: starlette==0.38.2; extra == "all"
|
|
243
|
+
Requires-Dist: twine; extra == "all"
|
|
231
244
|
Provides-Extra: auth
|
|
232
245
|
Requires-Dist: google-cloud-firestore<3,>=2.3; extra == "auth"
|
|
233
246
|
Provides-Extra: buildtools
|
|
@@ -247,24 +260,10 @@ Requires-Dist: google-cloud-tasks<3,>=2.7; extra == "tasks"
|
|
|
247
260
|
Provides-Extra: toolkit
|
|
248
261
|
Requires-Dist: boto3; extra == "toolkit"
|
|
249
262
|
Requires-Dist: google-cloud-storage<3,>=2.1; extra == "toolkit"
|
|
250
|
-
Requires-Dist: mutagen~=1.47.0; extra == "toolkit"
|
|
251
263
|
Requires-Dist: playwright~=1.51.0; extra == "toolkit"
|
|
252
264
|
Requires-Dist: sh~=2.0.6; extra == "toolkit"
|
|
253
|
-
Requires-Dist: tqdm~=4.66.1; extra == "toolkit"
|
|
254
265
|
Provides-Extra: web
|
|
255
266
|
Requires-Dist: google-cloud-error-reporting<2,>=1.9.2; extra == "web"
|
|
256
|
-
Provides-Extra: test
|
|
257
|
-
Requires-Dist: google-cloud-error-reporting<2,>=1.9.2; extra == "test"
|
|
258
|
-
Requires-Dist: google-cloud-firestore<3,>=2.3; extra == "test"
|
|
259
|
-
Requires-Dist: google-cloud-pubsub<3,>=2.9; extra == "test"
|
|
260
|
-
Requires-Dist: google-cloud-storage<3,>=2.1; extra == "test"
|
|
261
|
-
Requires-Dist: google-cloud-tasks<3,>=2.7; extra == "test"
|
|
262
|
-
Requires-Dist: httpx; extra == "test"
|
|
263
|
-
Requires-Dist: mutagen~=1.47.0; extra == "test"
|
|
264
|
-
Requires-Dist: playwright~=1.51.0; extra == "test"
|
|
265
|
-
Requires-Dist: sh~=2.0.6; extra == "test"
|
|
266
|
-
Requires-Dist: starlette==0.38.2; extra == "test"
|
|
267
|
-
Requires-Dist: tqdm~=4.66.1; extra == "test"
|
|
268
267
|
Dynamic: license-file
|
|
269
268
|
|
|
270
269
|
The library provides a collection of tools and functionality for the Scoota Platform services.
|
|
@@ -276,7 +275,7 @@ The library provides a collection of tools and functionality for the Scoota Plat
|
|
|
276
275
|
Install this library in a virtualenv using pip.
|
|
277
276
|
|
|
278
277
|
```
|
|
279
|
-
pip install rbx
|
|
278
|
+
pip install rbx[all]
|
|
280
279
|
```
|
|
281
280
|
|
|
282
281
|
> See the [wiki](https://github.com/rockabox/rbx/wiki) for more details on the contents of this library.
|
|
@@ -7,7 +7,7 @@ The library provides a collection of tools and functionality for the Scoota Plat
|
|
|
7
7
|
Install this library in a virtualenv using pip.
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
pip install rbx
|
|
10
|
+
pip install rbx[all]
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
> See the [wiki](https://github.com/rockabox/rbx/wiki) for more details on the contents of this library.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rbx"
|
|
7
|
-
version = "3.18.3"
|
|
7
|
+
version = "3.18.3.dev157"
|
|
8
8
|
description = "A collection of common tools for Scoota services."
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "The Scoota Engineering Team", email = "engineering@scoota.com" }
|
|
@@ -35,6 +35,20 @@ dependencies = [
|
|
|
35
35
|
]
|
|
36
36
|
|
|
37
37
|
[project.optional-dependencies]
|
|
38
|
+
all = [
|
|
39
|
+
"boto3",
|
|
40
|
+
"fabric~=3.2.0",
|
|
41
|
+
"google-cloud-error-reporting>=1.9.2,<2",
|
|
42
|
+
"google-cloud-firestore>=2.3,<3",
|
|
43
|
+
"google-cloud-pubsub>=2.9,<3",
|
|
44
|
+
"google-cloud-storage>=2.1,<3",
|
|
45
|
+
"google-cloud-tasks>=2.7,<3",
|
|
46
|
+
"Jinja2==3.1.2",
|
|
47
|
+
"playwright~=1.51.0",
|
|
48
|
+
"sh~=2.0.6",
|
|
49
|
+
"starlette==0.38.2",
|
|
50
|
+
"twine",
|
|
51
|
+
]
|
|
38
52
|
auth = [
|
|
39
53
|
"google-cloud-firestore>=2.3,<3"
|
|
40
54
|
]
|
|
@@ -60,27 +74,12 @@ tasks = [
|
|
|
60
74
|
toolkit = [
|
|
61
75
|
"boto3",
|
|
62
76
|
"google-cloud-storage>=2.1,<3",
|
|
63
|
-
"mutagen~=1.47.0",
|
|
64
77
|
"playwright~=1.51.0",
|
|
65
78
|
"sh~=2.0.6",
|
|
66
|
-
"tqdm~=4.66.1",
|
|
67
79
|
]
|
|
68
80
|
web = [
|
|
69
81
|
"google-cloud-error-reporting>=1.9.2,<2"
|
|
70
82
|
]
|
|
71
|
-
test = [
|
|
72
|
-
"google-cloud-error-reporting>=1.9.2,<2",
|
|
73
|
-
"google-cloud-firestore>=2.3,<3",
|
|
74
|
-
"google-cloud-pubsub>=2.9,<3",
|
|
75
|
-
"google-cloud-storage>=2.1,<3",
|
|
76
|
-
"google-cloud-tasks>=2.7,<3",
|
|
77
|
-
"httpx",
|
|
78
|
-
"mutagen~=1.47.0",
|
|
79
|
-
"playwright~=1.51.0",
|
|
80
|
-
"sh~=2.0.6",
|
|
81
|
-
"starlette==0.38.2",
|
|
82
|
-
"tqdm~=4.66.1",
|
|
83
|
-
]
|
|
84
83
|
|
|
85
84
|
[project.scripts]
|
|
86
85
|
buildtools = "rbx.buildtools.cli:cli [buildtools]"
|
|
@@ -91,7 +90,7 @@ homepage = "https://github.com/rockabox/rbx"
|
|
|
91
90
|
repository = "https://github.com/rockabox/rbx.git"
|
|
92
91
|
|
|
93
92
|
[tool.bumpversion]
|
|
94
|
-
current_version = "3.18.3"
|
|
93
|
+
current_version = "3.18.3.dev157"
|
|
95
94
|
commit = true
|
|
96
95
|
parse = """
|
|
97
96
|
(?P<major>\\d+)\\.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.18.3.dev157"
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
__all__ = ["Options", "run"]
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
import shutil
|
|
5
4
|
import tempfile
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
from typing import NamedTuple, Optional
|
|
8
7
|
|
|
9
|
-
import mutagen
|
|
10
|
-
|
|
11
8
|
from .browser import record, screenshot
|
|
12
|
-
from .media import convert, merge
|
|
13
9
|
from .utils import upload
|
|
14
|
-
from .web import download
|
|
15
10
|
|
|
16
11
|
logger = logging.getLogger(__name__)
|
|
17
12
|
|
|
@@ -35,52 +30,16 @@ def filename(output, filename):
|
|
|
35
30
|
return str(Path(output) / filename)
|
|
36
31
|
|
|
37
32
|
|
|
38
|
-
def add_audio(filename: str, path: Path) -> None:
|
|
39
|
-
"""Add audio stream.
|
|
40
|
-
|
|
41
|
-
The source of the audio is extracted from a "source.mp4" video file found in the `path`.
|
|
42
|
-
The video file is first renamed because FFmpeg cannot edit existing files in-place.
|
|
43
|
-
"""
|
|
44
|
-
source = str(path / "source.mp4")
|
|
45
|
-
audio = str(path / "audio.mp3")
|
|
46
|
-
convert(infile=source, outfile=audio)
|
|
47
|
-
|
|
48
|
-
video = str(path / f"_{filename}")
|
|
49
|
-
shutil.move(path / filename, video)
|
|
50
|
-
merge(infiles=[video, audio], outfile=str(path / filename))
|
|
51
|
-
|
|
52
|
-
|
|
53
33
|
async def capture(filename: str, options: Options, path: Path) -> None:
|
|
54
|
-
|
|
34
|
+
await record(
|
|
55
35
|
dirname=str(path),
|
|
36
|
+
filename=str(path / filename),
|
|
56
37
|
duration=1000 * options.duration,
|
|
57
38
|
height=options.height,
|
|
58
39
|
url=options.url,
|
|
59
40
|
width=options.width,
|
|
60
41
|
)
|
|
61
42
|
|
|
62
|
-
audio = None
|
|
63
|
-
if recording.source:
|
|
64
|
-
logger.debug(f"Downloading video source from {recording.source}")
|
|
65
|
-
audio = path / "source.mp4"
|
|
66
|
-
download(url=recording.source, filename=str(audio))
|
|
67
|
-
file = mutagen.File(audio)
|
|
68
|
-
if file.info.channels == 0:
|
|
69
|
-
logger.debug("Video source has no audio")
|
|
70
|
-
audio = None
|
|
71
|
-
|
|
72
|
-
convert(
|
|
73
|
-
infile=recording.location,
|
|
74
|
-
outfile=str(path / filename),
|
|
75
|
-
delay=recording.delay,
|
|
76
|
-
duration=1000 * options.duration,
|
|
77
|
-
# use highest quality, and place MOOV atom at the beginning
|
|
78
|
-
opts=["-crf", "1", "-movflags", "faststart"],
|
|
79
|
-
)
|
|
80
|
-
if audio:
|
|
81
|
-
logger.info(f"Adding audio from {audio}")
|
|
82
|
-
add_audio(filename=filename, path=path)
|
|
83
|
-
|
|
84
43
|
|
|
85
44
|
async def screengrab(filename: str, options: Options, path: Path) -> None:
|
|
86
45
|
await screenshot(
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import signal
|
|
5
|
+
import subprocess
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
|
|
9
|
+
from playwright.async_api import async_playwright
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_free_display(base=99, max_tries=100):
|
|
15
|
+
for i in range(max_tries):
|
|
16
|
+
display = f":{base + i}"
|
|
17
|
+
if not os.path.exists(f"/tmp/.X{base + i}-lock"):
|
|
18
|
+
return display
|
|
19
|
+
|
|
20
|
+
raise RuntimeError("No free display found")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RecordingSession:
|
|
24
|
+
def __init__(self, dirname, width, height):
|
|
25
|
+
self.display = get_free_display()
|
|
26
|
+
self.dirname = dirname
|
|
27
|
+
self.width = width
|
|
28
|
+
self.height = height
|
|
29
|
+
self.resolution = f"{self.width}x{self.height}"
|
|
30
|
+
self.session_id = str(uuid.uuid4())[:8]
|
|
31
|
+
self.sink_name = f"rec_sink_{self.session_id}"
|
|
32
|
+
self.pulse_dir = f"{dirname}/pulseaudio"
|
|
33
|
+
self.pulse_socket = os.path.join(self.pulse_dir, "pulse-socket")
|
|
34
|
+
self.proc = []
|
|
35
|
+
|
|
36
|
+
async def __aenter__(self):
|
|
37
|
+
return self
|
|
38
|
+
|
|
39
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
40
|
+
logger.debug("Stopping processes...")
|
|
41
|
+
|
|
42
|
+
# PulseAudio needs to be terminated via the kill command.
|
|
43
|
+
subprocess.Popen(
|
|
44
|
+
["pulseaudio", "--kill"],
|
|
45
|
+
env={
|
|
46
|
+
"PULSE_SERVER": f"unix:{self.pulse_socket}",
|
|
47
|
+
"XDG_RUNTIME_DIR": self.pulse_dir,
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
for proc in reversed(self.proc):
|
|
52
|
+
if proc and proc.poll() is None:
|
|
53
|
+
try:
|
|
54
|
+
proc.send_signal(signal.SIGINT)
|
|
55
|
+
proc.wait(timeout=3)
|
|
56
|
+
except subprocess.TimeoutExpired:
|
|
57
|
+
proc.kill()
|
|
58
|
+
|
|
59
|
+
def start_xvfb(self):
|
|
60
|
+
logger.debug(f"Starting Xvfb [{self.display}]...")
|
|
61
|
+
# fmt: off
|
|
62
|
+
self.proc.append(
|
|
63
|
+
subprocess.Popen(
|
|
64
|
+
[
|
|
65
|
+
"Xvfb", self.display, "-screen", "0", f"{self.resolution}x24",
|
|
66
|
+
# These options are needed when running in a container
|
|
67
|
+
"-ac", "-nolisten", "tcp", "-nolisten", "unix",
|
|
68
|
+
]
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
# fmt: on
|
|
72
|
+
time.sleep(1)
|
|
73
|
+
|
|
74
|
+
logger.debug(f"Starting unclutter [{self.display}]...")
|
|
75
|
+
self.proc.append(
|
|
76
|
+
subprocess.Popen(
|
|
77
|
+
["unclutter", "-idle", "0"],
|
|
78
|
+
env={"DISPLAY": self.display},
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def start_pulseaudio(self):
|
|
83
|
+
logger.debug(f"Starting PulseAudio [{self.pulse_dir}.{self.sink_name}]...")
|
|
84
|
+
os.makedirs(self.pulse_dir, exist_ok=True)
|
|
85
|
+
os.chmod(self.pulse_dir, 0o700) # PulseAudio requires strict permissions
|
|
86
|
+
|
|
87
|
+
subprocess.Popen(
|
|
88
|
+
[
|
|
89
|
+
"pulseaudio",
|
|
90
|
+
"--start",
|
|
91
|
+
"--exit-idle-time=300",
|
|
92
|
+
f"--load=module-native-protocol-unix socket={self.pulse_socket}",
|
|
93
|
+
f"--load=module-null-sink sink_name={self.sink_name}"
|
|
94
|
+
f" sink_properties=device.description={self.sink_name}",
|
|
95
|
+
],
|
|
96
|
+
env={"DISPLAY": self.display, "XDG_RUNTIME_DIR": self.pulse_dir},
|
|
97
|
+
)
|
|
98
|
+
time.sleep(2)
|
|
99
|
+
|
|
100
|
+
def start_ffmpeg(self, duration, output_path):
|
|
101
|
+
logger.debug(f"Starting FFmpeg recording [{self.display}:{self.sink_name}]...")
|
|
102
|
+
# fmt: off
|
|
103
|
+
self.proc.append(
|
|
104
|
+
subprocess.Popen(
|
|
105
|
+
[
|
|
106
|
+
"ffmpeg", "-y",
|
|
107
|
+
"-t", str(duration),
|
|
108
|
+
"-video_size", self.resolution,
|
|
109
|
+
"-f", "x11grab",
|
|
110
|
+
"-i", f"{self.display}.0",
|
|
111
|
+
"-f", "pulse",
|
|
112
|
+
"-i", f"{self.sink_name}.monitor",
|
|
113
|
+
"-c:v", "libx264",
|
|
114
|
+
"-preset", "slow",
|
|
115
|
+
"-crf", "8",
|
|
116
|
+
"-pix_fmt", "yuv420p",
|
|
117
|
+
"-c:a", "aac",
|
|
118
|
+
"-b:a", "256k",
|
|
119
|
+
"-movflags",
|
|
120
|
+
"+faststart",
|
|
121
|
+
output_path,
|
|
122
|
+
],
|
|
123
|
+
stdout=subprocess.DEVNULL,
|
|
124
|
+
stderr=subprocess.STDOUT,
|
|
125
|
+
env={
|
|
126
|
+
"PULSE_SERVER": f"unix:{self.pulse_socket}",
|
|
127
|
+
"XDG_RUNTIME_DIR": self.pulse_dir,
|
|
128
|
+
},
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
# fmt: on
|
|
132
|
+
|
|
133
|
+
async def capture(self, url, duration, output_path):
|
|
134
|
+
self.start_xvfb()
|
|
135
|
+
self.start_pulseaudio()
|
|
136
|
+
|
|
137
|
+
async with async_playwright() as p:
|
|
138
|
+
logger.debug(
|
|
139
|
+
f"Capturing Chromium session [{self.display}:{self.sink_name}]..."
|
|
140
|
+
)
|
|
141
|
+
context = await p.chromium.launch_persistent_context(
|
|
142
|
+
user_data_dir=f"{self.dirname}/user-data",
|
|
143
|
+
headless=False,
|
|
144
|
+
channel="chrome",
|
|
145
|
+
args=[
|
|
146
|
+
"--no-sandbox",
|
|
147
|
+
"--disable-gpu",
|
|
148
|
+
"--disable-software-rasterizer",
|
|
149
|
+
f"--display={self.display}",
|
|
150
|
+
"--autoplay-policy=no-user-gesture-required",
|
|
151
|
+
"--use-fake-ui-for-media-stream",
|
|
152
|
+
f"--alsa-output-device={self.sink_name}",
|
|
153
|
+
"--start-fullscreen",
|
|
154
|
+
"--window-position=0,0",
|
|
155
|
+
f"--window-size={self.width},{self.height}",
|
|
156
|
+
],
|
|
157
|
+
ignore_default_args=["--enable-automation"],
|
|
158
|
+
no_viewport=True,
|
|
159
|
+
env={
|
|
160
|
+
"PULSE_SERVER": f"unix:{self.pulse_socket}",
|
|
161
|
+
"XDG_RUNTIME_DIR": self.pulse_dir,
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
page = await context.new_page()
|
|
165
|
+
await page.goto(url, wait_until="commit")
|
|
166
|
+
|
|
167
|
+
logger.info(
|
|
168
|
+
f"Recording session [{self.display}:{self.sink_name}] for {duration}ms"
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
self.start_ffmpeg(duration, output_path)
|
|
172
|
+
|
|
173
|
+
await page.wait_for_timeout(duration) # Wait for the duration
|
|
174
|
+
await context.close()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
async def record(
|
|
178
|
+
dirname: str, filename: str, duration: int, height: int, url: str, width: int
|
|
179
|
+
) -> None:
|
|
180
|
+
async with RecordingSession(dirname, width, height) as session:
|
|
181
|
+
await session.capture(url, duration, filename)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
async def screenshot(filename: str, height: int, url: str, width: int) -> None:
|
|
185
|
+
async with async_playwright() as p:
|
|
186
|
+
browser = await p.chromium.launch()
|
|
187
|
+
page = await browser.new_page()
|
|
188
|
+
await page.set_viewport_size({"width": width, "height": height})
|
|
189
|
+
await page.goto(url, wait_until="networkidle")
|
|
190
|
+
await page.screenshot(path=filename)
|
|
191
|
+
await browser.close()
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def to_ms(timedelta: datetime.timedelta) -> int:
|
|
195
|
+
return round(1000 * timedelta.total_seconds())
|
|
@@ -23,21 +23,13 @@ def convert(
|
|
|
23
23
|
if duration:
|
|
24
24
|
args.extend(["-to", milliseconds_to_duration(delay + duration)])
|
|
25
25
|
if delay or duration:
|
|
26
|
-
args.extend(["-c:v", "libx264", "-preset", "slow", "-crf", "
|
|
26
|
+
args.extend(["-c:v", "libx264", "-preset", "slow", "-crf", "8"])
|
|
27
27
|
if opts:
|
|
28
28
|
args.extend(opts)
|
|
29
29
|
args.append(outfile)
|
|
30
30
|
ffmpg(args)
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
def merge(infiles: [str], outfile: str) -> None:
|
|
34
|
-
args = ["-y"]
|
|
35
|
-
for file in infiles:
|
|
36
|
-
args.extend(["-i", file])
|
|
37
|
-
args.extend(["-c", "copy", outfile])
|
|
38
|
-
ffmpg(args)
|
|
39
|
-
|
|
40
|
-
|
|
41
33
|
def ffmpg(args):
|
|
42
34
|
try:
|
|
43
35
|
sh.ffmpeg(*args)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rbx
|
|
3
|
-
Version: 3.18.3
|
|
3
|
+
Version: 3.18.3.dev157
|
|
4
4
|
Summary: A collection of common tools for Scoota services.
|
|
5
5
|
Author-email: The Scoota Engineering Team <engineering@scoota.com>
|
|
6
6
|
License:
|
|
@@ -228,6 +228,19 @@ Requires-Dist: Click<9
|
|
|
228
228
|
Requires-Dist: colorama
|
|
229
229
|
Requires-Dist: lxml<6,>=5.3.0
|
|
230
230
|
Requires-Dist: requests>=2.31.0
|
|
231
|
+
Provides-Extra: all
|
|
232
|
+
Requires-Dist: boto3; extra == "all"
|
|
233
|
+
Requires-Dist: fabric~=3.2.0; extra == "all"
|
|
234
|
+
Requires-Dist: google-cloud-error-reporting<2,>=1.9.2; extra == "all"
|
|
235
|
+
Requires-Dist: google-cloud-firestore<3,>=2.3; extra == "all"
|
|
236
|
+
Requires-Dist: google-cloud-pubsub<3,>=2.9; extra == "all"
|
|
237
|
+
Requires-Dist: google-cloud-storage<3,>=2.1; extra == "all"
|
|
238
|
+
Requires-Dist: google-cloud-tasks<3,>=2.7; extra == "all"
|
|
239
|
+
Requires-Dist: Jinja2==3.1.2; extra == "all"
|
|
240
|
+
Requires-Dist: playwright~=1.51.0; extra == "all"
|
|
241
|
+
Requires-Dist: sh~=2.0.6; extra == "all"
|
|
242
|
+
Requires-Dist: starlette==0.38.2; extra == "all"
|
|
243
|
+
Requires-Dist: twine; extra == "all"
|
|
231
244
|
Provides-Extra: auth
|
|
232
245
|
Requires-Dist: google-cloud-firestore<3,>=2.3; extra == "auth"
|
|
233
246
|
Provides-Extra: buildtools
|
|
@@ -247,24 +260,10 @@ Requires-Dist: google-cloud-tasks<3,>=2.7; extra == "tasks"
|
|
|
247
260
|
Provides-Extra: toolkit
|
|
248
261
|
Requires-Dist: boto3; extra == "toolkit"
|
|
249
262
|
Requires-Dist: google-cloud-storage<3,>=2.1; extra == "toolkit"
|
|
250
|
-
Requires-Dist: mutagen~=1.47.0; extra == "toolkit"
|
|
251
263
|
Requires-Dist: playwright~=1.51.0; extra == "toolkit"
|
|
252
264
|
Requires-Dist: sh~=2.0.6; extra == "toolkit"
|
|
253
|
-
Requires-Dist: tqdm~=4.66.1; extra == "toolkit"
|
|
254
265
|
Provides-Extra: web
|
|
255
266
|
Requires-Dist: google-cloud-error-reporting<2,>=1.9.2; extra == "web"
|
|
256
|
-
Provides-Extra: test
|
|
257
|
-
Requires-Dist: google-cloud-error-reporting<2,>=1.9.2; extra == "test"
|
|
258
|
-
Requires-Dist: google-cloud-firestore<3,>=2.3; extra == "test"
|
|
259
|
-
Requires-Dist: google-cloud-pubsub<3,>=2.9; extra == "test"
|
|
260
|
-
Requires-Dist: google-cloud-storage<3,>=2.1; extra == "test"
|
|
261
|
-
Requires-Dist: google-cloud-tasks<3,>=2.7; extra == "test"
|
|
262
|
-
Requires-Dist: httpx; extra == "test"
|
|
263
|
-
Requires-Dist: mutagen~=1.47.0; extra == "test"
|
|
264
|
-
Requires-Dist: playwright~=1.51.0; extra == "test"
|
|
265
|
-
Requires-Dist: sh~=2.0.6; extra == "test"
|
|
266
|
-
Requires-Dist: starlette==0.38.2; extra == "test"
|
|
267
|
-
Requires-Dist: tqdm~=4.66.1; extra == "test"
|
|
268
267
|
Dynamic: license-file
|
|
269
268
|
|
|
270
269
|
The library provides a collection of tools and functionality for the Scoota Platform services.
|
|
@@ -276,7 +275,7 @@ The library provides a collection of tools and functionality for the Scoota Plat
|
|
|
276
275
|
Install this library in a virtualenv using pip.
|
|
277
276
|
|
|
278
277
|
```
|
|
279
|
-
pip install rbx
|
|
278
|
+
pip install rbx[all]
|
|
280
279
|
```
|
|
281
280
|
|
|
282
281
|
> See the [wiki](https://github.com/rockabox/rbx/wiki) for more details on the contents of this library.
|
|
@@ -5,6 +5,20 @@ colorama
|
|
|
5
5
|
lxml<6,>=5.3.0
|
|
6
6
|
requests>=2.31.0
|
|
7
7
|
|
|
8
|
+
[all]
|
|
9
|
+
boto3
|
|
10
|
+
fabric~=3.2.0
|
|
11
|
+
google-cloud-error-reporting<2,>=1.9.2
|
|
12
|
+
google-cloud-firestore<3,>=2.3
|
|
13
|
+
google-cloud-pubsub<3,>=2.9
|
|
14
|
+
google-cloud-storage<3,>=2.1
|
|
15
|
+
google-cloud-tasks<3,>=2.7
|
|
16
|
+
Jinja2==3.1.2
|
|
17
|
+
playwright~=1.51.0
|
|
18
|
+
sh~=2.0.6
|
|
19
|
+
starlette==0.38.2
|
|
20
|
+
twine
|
|
21
|
+
|
|
8
22
|
[auth]
|
|
9
23
|
google-cloud-firestore<3,>=2.3
|
|
10
24
|
|
|
@@ -27,26 +41,11 @@ google-cloud-storage<3,>=2.1
|
|
|
27
41
|
[tasks]
|
|
28
42
|
google-cloud-tasks<3,>=2.7
|
|
29
43
|
|
|
30
|
-
[test]
|
|
31
|
-
google-cloud-error-reporting<2,>=1.9.2
|
|
32
|
-
google-cloud-firestore<3,>=2.3
|
|
33
|
-
google-cloud-pubsub<3,>=2.9
|
|
34
|
-
google-cloud-storage<3,>=2.1
|
|
35
|
-
google-cloud-tasks<3,>=2.7
|
|
36
|
-
httpx
|
|
37
|
-
mutagen~=1.47.0
|
|
38
|
-
playwright~=1.51.0
|
|
39
|
-
sh~=2.0.6
|
|
40
|
-
starlette==0.38.2
|
|
41
|
-
tqdm~=4.66.1
|
|
42
|
-
|
|
43
44
|
[toolkit]
|
|
44
45
|
boto3
|
|
45
46
|
google-cloud-storage<3,>=2.1
|
|
46
|
-
mutagen~=1.47.0
|
|
47
47
|
playwright~=1.51.0
|
|
48
48
|
sh~=2.0.6
|
|
49
|
-
tqdm~=4.66.1
|
|
50
49
|
|
|
51
50
|
[web]
|
|
52
51
|
google-cloud-error-reporting<2,>=1.9.2
|
rbx-3.18.3/rbx/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "3.18.3"
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
import logging
|
|
3
|
-
from typing import NamedTuple, Optional
|
|
4
|
-
|
|
5
|
-
from playwright.async_api import async_playwright
|
|
6
|
-
|
|
7
|
-
logger = logging.getLogger(__name__)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class Continue(Exception):
|
|
11
|
-
pass
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class Recording(NamedTuple):
|
|
15
|
-
location: str
|
|
16
|
-
delay: Optional[int] = 0
|
|
17
|
-
source: Optional[str] = None
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
async def record(
|
|
21
|
-
dirname: str, duration: int, height: int, url: str, width: int
|
|
22
|
-
) -> Recording:
|
|
23
|
-
async with async_playwright() as p:
|
|
24
|
-
browser = await p.chromium.launch(
|
|
25
|
-
args=["--autoplay-policy=no-user-gesture-required"]
|
|
26
|
-
)
|
|
27
|
-
context = await browser.new_context(
|
|
28
|
-
record_video_dir=dirname,
|
|
29
|
-
record_video_size={"width": width, "height": height},
|
|
30
|
-
viewport={"width": width, "height": height},
|
|
31
|
-
)
|
|
32
|
-
page = await context.new_page()
|
|
33
|
-
await page.set_viewport_size({"width": width, "height": height})
|
|
34
|
-
await page.goto(url, wait_until="domcontentloaded")
|
|
35
|
-
start = datetime.datetime.now(datetime.UTC)
|
|
36
|
-
|
|
37
|
-
try:
|
|
38
|
-
# Wait for the appropriate events to be seen in the console log.
|
|
39
|
-
# These events are expected to occur within 5 seconds, so we never wait any longer
|
|
40
|
-
# than that.
|
|
41
|
-
has_video = False
|
|
42
|
-
ready = None
|
|
43
|
-
while True:
|
|
44
|
-
async with page.expect_console_message(timeout=0) as msg_info:
|
|
45
|
-
message = await msg_info.value
|
|
46
|
-
if message.type == "log":
|
|
47
|
-
for arg in message.args:
|
|
48
|
-
line = await arg.json_value()
|
|
49
|
-
if "/session" in line:
|
|
50
|
-
ready = datetime.datetime.now(datetime.UTC)
|
|
51
|
-
elif "action=p0" in line or "action=p10" in line:
|
|
52
|
-
has_video = True
|
|
53
|
-
|
|
54
|
-
if (ready and has_video) or (
|
|
55
|
-
datetime.datetime.now(datetime.UTC) - start
|
|
56
|
-
).total_seconds() >= 5:
|
|
57
|
-
ready = ready or start
|
|
58
|
-
raise Continue
|
|
59
|
-
|
|
60
|
-
except Continue:
|
|
61
|
-
checkpoint = datetime.datetime.now(datetime.UTC)
|
|
62
|
-
text = "with" if has_video else "without"
|
|
63
|
-
logger.debug(f"Recording {text} video @ {to_ms(checkpoint - start)}ms")
|
|
64
|
-
src = None
|
|
65
|
-
|
|
66
|
-
if has_video:
|
|
67
|
-
video = page.locator("video")
|
|
68
|
-
await video.wait_for(timeout=2000)
|
|
69
|
-
for source in await video.locator("source").all():
|
|
70
|
-
if await source.get_attribute("type") == "video/mp4":
|
|
71
|
-
value = await source.get_attribute("src")
|
|
72
|
-
src, _, _ = value.partition("?")
|
|
73
|
-
|
|
74
|
-
# Record until the end of the video, at which point the `video` element will be set
|
|
75
|
-
# as hidden.
|
|
76
|
-
await page.locator("video").first.wait_for(
|
|
77
|
-
state="hidden", timeout=60000
|
|
78
|
-
)
|
|
79
|
-
checkpoint = datetime.datetime.now(datetime.UTC)
|
|
80
|
-
finished = datetime.datetime.now(datetime.UTC)
|
|
81
|
-
|
|
82
|
-
# Continue recording until the required duration.
|
|
83
|
-
checkpoint = datetime.datetime.now(datetime.UTC)
|
|
84
|
-
elapsed = int(1000 * (checkpoint - ready).total_seconds())
|
|
85
|
-
remaining = duration - elapsed
|
|
86
|
-
if remaining > 0:
|
|
87
|
-
logger.debug(f"{remaining}ms left to record")
|
|
88
|
-
expression = (
|
|
89
|
-
"window.recording = 1; setTimeout(() => { window.recording = 0 }, "
|
|
90
|
-
+ str(remaining)
|
|
91
|
-
+ ");"
|
|
92
|
-
)
|
|
93
|
-
await page.evaluate(expression)
|
|
94
|
-
await page.wait_for_function("() => window.recording == 0")
|
|
95
|
-
else:
|
|
96
|
-
logger.debug("Nothing left to record")
|
|
97
|
-
|
|
98
|
-
finished = datetime.datetime.now(datetime.UTC)
|
|
99
|
-
|
|
100
|
-
else:
|
|
101
|
-
elapsed = int(1000 * (checkpoint - start).total_seconds())
|
|
102
|
-
remaining = duration - elapsed
|
|
103
|
-
if remaining > 0:
|
|
104
|
-
logger.debug(f"{remaining}ms left to record")
|
|
105
|
-
expression = (
|
|
106
|
-
"window.recording = 1; setTimeout(() => { window.recording = 0 }, "
|
|
107
|
-
+ str(remaining)
|
|
108
|
-
+ ");"
|
|
109
|
-
)
|
|
110
|
-
await page.evaluate(expression)
|
|
111
|
-
await page.wait_for_function("() => window.recording == 0")
|
|
112
|
-
else:
|
|
113
|
-
logger.debug("Nothing left to record")
|
|
114
|
-
|
|
115
|
-
finished = datetime.datetime.now(datetime.UTC)
|
|
116
|
-
|
|
117
|
-
logger.debug(f"Finished @ {to_ms(finished - ready)}ms")
|
|
118
|
-
await context.close()
|
|
119
|
-
location = await page.video.path()
|
|
120
|
-
await browser.close()
|
|
121
|
-
|
|
122
|
-
# The delay is the lag between the start of the recording session and the time the video
|
|
123
|
-
# started playing.
|
|
124
|
-
delay = to_ms(ready - start)
|
|
125
|
-
logger.debug(f"Recorded with delay of {delay}ms")
|
|
126
|
-
|
|
127
|
-
return Recording(delay=delay, location=location, source=src)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
async def screenshot(filename: str, height: int, url: str, width: int) -> None:
|
|
131
|
-
async with async_playwright() as p:
|
|
132
|
-
browser = await p.chromium.launch()
|
|
133
|
-
page = await browser.new_page()
|
|
134
|
-
await page.set_viewport_size({"width": width, "height": height})
|
|
135
|
-
await page.goto(url, wait_until="networkidle")
|
|
136
|
-
await page.screenshot(path=filename)
|
|
137
|
-
await browser.close()
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def to_ms(timedelta: datetime.timedelta) -> int:
|
|
141
|
-
return round(1000 * timedelta.total_seconds())
|
rbx-3.18.3/rbx/toolkit/web.py
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|