mirror of
https://github.com/saphid/frame-control.git
synced 2026-10-24 10:02:26 +02:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
040581a4c7 | ||
|
|
17f6d52717 | ||
|
|
b4e80cab0a | ||
|
|
b98e3e496f | ||
|
|
a148e02d39 |
No files matched your search
@@ -2,6 +2,9 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Treat headset aliases and other CSV metadata as plain text in power and heat
|
||||
exports, so spreadsheet formula prefixes cannot run when a report is opened.
|
||||
|
||||
- Add desktop Tools → Power & heat report: bounded read-only one/five-minute
|
||||
observations with progress, stop and HTML/JSON/CSV downloads. Received samples
|
||||
survive stop, disconnect and headset switch, labeled incomplete and tied to the
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "frame-control",
|
||||
"version": "0.4.14",
|
||||
"version": "0.4.16",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frame-control",
|
||||
"version": "0.4.14",
|
||||
"version": "0.4.16",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"electron": "^44.4.5",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frame-control",
|
||||
"productName": "Frame Control",
|
||||
"version": "0.4.14",
|
||||
"version": "0.4.16",
|
||||
"description": "Desktop app for managing a Valve Steam Frame over SSH",
|
||||
"private": true,
|
||||
"main": "main.js",
|
||||
|
||||
@@ -94,7 +94,9 @@ in your browser to view the offline table and its embedded JSON/CSV downloads.
|
||||
Snapshots taken before completion explicitly say incomplete. Desktop CSV includes
|
||||
`collection.state`, `collection.requestedSamples` and `collection.receivedSamples`
|
||||
metadata rows with the original headset alias in the source column, even when no
|
||||
sensor sample arrived. JSON records the alias and device ID; HTML names the alias.
|
||||
sensor sample arrived. CSV text fields, including the alias, are escaped if they
|
||||
begin with a spreadsheet formula prefix. JSON records the alias and device ID;
|
||||
HTML names the alias.
|
||||
|
||||
Only the latest observation is retained in server memory, including partial
|
||||
samples after stop or disconnect. **Download before starting another observation,
|
||||
|
||||
+28
-16
@@ -8,13 +8,13 @@ Both are on **Tools → Capture & Live**. Everything is saved to
|
||||
Nothing needs installing on the computer or on the Frame: the work is done by
|
||||
the `ffmpeg` that SteamOS ships.
|
||||
|
||||
**Recording is tested on a headset; Steam clip export isn't yet.** On a Frame
|
||||
**Recording and Steam clip export have each been tested on a headset.** On a Frame
|
||||
running SteamOS 0.5.6 (build 20261015.6141207), Record and Stop saved an
|
||||
8-second 1080p H.264 recording with AAC sound to the computer, Stop took about a
|
||||
second, and nothing was left running or stored on the Frame. That Frame had no
|
||||
Steam recordings, so the clip list came back empty and export has only been
|
||||
tested with files in Steam's format made by `ffmpeg`. The parts we're least sure
|
||||
of are listed under [What hasn't been checked](#what-hasnt-been-checked).
|
||||
second, and nothing was left running or stored on the Frame. A later test on
|
||||
that build created a saved clip with Steam's own recorder and exported it with
|
||||
Frame Control's production export function. The parts we're least sure of are
|
||||
listed under [What hasn't been checked](#what-hasnt-been-checked).
|
||||
|
||||
## Record
|
||||
|
||||
@@ -147,19 +147,31 @@ stand-in `ffmpeg`, so Stop, the time limit and the clean-up are tested as they
|
||||
run on the Frame. Where `ffmpeg` is installed, they also make a 65-second DASH
|
||||
recording with Steam's file names and export it with the real `ffmpeg`.
|
||||
|
||||
## What hasn't been checked
|
||||
## Real Steam clip verification and remaining checks
|
||||
|
||||
The 2026-10-24 read-only clip list on build `20261015.6141207` again returned
|
||||
zero clips: its `gamerecordings` directory held only `gamerecording.pb`. No
|
||||
Steam recording was created during that check, so a real Steam clip export is
|
||||
still unverified. The recording and Stop path were verified earlier on that
|
||||
build, as described above. Remaining checks:
|
||||
On 2026-10-24, v0.4.15 on build `20261015.6141207` recorded a ten-second
|
||||
reserved test session through Steam's own game recorder, saved it as a clip,
|
||||
then exported it with `frame_clips.export_clip`, the same implementation used
|
||||
by the desktop API. The 159,829-byte MP4 decoded completely: 10.29 seconds,
|
||||
1920×1080 H.264 video and stereo AAC audio (SHA-256
|
||||
`f8a476eefb2bde7ba5cb464632ec9b952eea3ab1610b5bed2098bf1f11d62cf1`).
|
||||
All 616 decoded video frames and the decoded audio matched Steam's source
|
||||
segments. Recording mode was restored
|
||||
to off; the reserved title and its processes were removed, the shared test lock
|
||||
was released, and the battery remained at 99%. The Steam source clip was kept.
|
||||
|
||||
- Audio and video sync. Both inputs are stamped with the wall clock, because
|
||||
the camera device and the audio server otherwise count from different clocks.
|
||||
- The exact layout of `gamerecordings` on the Frame's SteamOS build, and
|
||||
whether a clip's segments start mid-recording. The parser is deliberately
|
||||
loose about the layout.
|
||||
The inspected frame is nearly black in both Steam's source and the export, so
|
||||
this establishes file creation and faithful export, **not visible gameplay
|
||||
capture**. The literal desktop **Export** button was not clicked. Remaining
|
||||
checks include:
|
||||
|
||||
- Perceptual audio/video sync and audible content. Frame Control's separate
|
||||
**Record** path stamps camera and audio inputs with the wall clock because
|
||||
they otherwise count from different clocks.
|
||||
- Long saved clips, trims starting partway through a Steam recording, and the
|
||||
desktop **Export** button. One real saved clip's
|
||||
directory layout was checked; the parser remains deliberately loose for
|
||||
other layouts.
|
||||
- How much x264 at 1080p30 costs the Frame while a game is running.
|
||||
- Automatic copying: that 30 seconds without a write is a safe sign Steam has
|
||||
finished saving a clip, and how a once-a-minute listing feels on the Frame.
|
||||
@@ -141,6 +141,13 @@ def collect(status, count, interval, emit=None):
|
||||
|
||||
|
||||
def csv_samples(report):
|
||||
# CSV quoting alone does not prevent spreadsheet formulas in text fields.
|
||||
def cell(value):
|
||||
if isinstance(value, str) and (value.startswith(('\t', '\r', '\n')) or
|
||||
value.lstrip(' \t\r\n').startswith(('=', '+', '-', '@'))):
|
||||
return "'" + value
|
||||
return value
|
||||
|
||||
output = io.StringIO(newline='')
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(('time', 'elapsedSeconds', 'metric', 'value', 'unit', 'source', 'state'))
|
||||
@@ -148,17 +155,14 @@ def csv_samples(report):
|
||||
for key, value in (('collection.state', report['stopped'] or 'Completed'),
|
||||
('collection.requestedSamples', report['requestedSamples']),
|
||||
('collection.receivedSamples', len(report['samples']))):
|
||||
writer.writerow(('', '', key, value, '', report['headset']['alias'],
|
||||
'incomplete' if report['stopped'] else 'completed'))
|
||||
writer.writerow([cell(v) for v in ('', '', key, value, '', report['headset']['alias'],
|
||||
'incomplete' if report['stopped'] else 'completed')])
|
||||
for sample in report['samples']:
|
||||
for key, row in sample['readings'].items():
|
||||
# csv quoting does not prevent spreadsheet formulas in labels.
|
||||
def cell(value):
|
||||
return "'" + value if isinstance(value, str) and value.startswith(('=', '+', '-', '@')) else value
|
||||
writer.writerow([cell(v) for v in (sample['time'], sample['elapsedSeconds'], key,
|
||||
row['value'], row['unit'], row['source'], row['state'])])
|
||||
for group in sample['unavailableGroups']:
|
||||
writer.writerow((sample['time'], sample['elapsedSeconds'], group, '', '', '', UNAVAILABLE))
|
||||
writer.writerow([cell(v) for v in (sample['time'], sample['elapsedSeconds'], group, '', '', '', UNAVAILABLE)])
|
||||
return output.getvalue()
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import sandbox # noqa: F401
|
||||
import base64
|
||||
import contextlib
|
||||
import csv
|
||||
from html.parser import HTMLParser
|
||||
import io
|
||||
import json
|
||||
@@ -43,6 +44,19 @@ class Report(unittest.TestCase):
|
||||
with contextlib.redirect_stderr(io.StringIO()):
|
||||
return power.stream_report([sys.executable, '-c', program], '', count, 1)
|
||||
|
||||
def test_csv_metadata_headset_alias_is_plain_text_in_spreadsheets(self):
|
||||
for prefix in ('=', '+', '-', '@', ' =', '\t='):
|
||||
with self.subTest(prefix=prefix):
|
||||
alias = prefix + 'HYPERLINK("https://example.invalid")'
|
||||
report = {'headset': {'alias': alias}, 'requestedSamples': 12,
|
||||
'stopped': 'Stopped', 'samples': []}
|
||||
rows = list(csv.reader(io.StringIO(power.csv_samples(report))))
|
||||
self.assertEqual(len(rows), 4)
|
||||
self.assertEqual([row[5] for row in rows[1:]], ["'" + alias] * 3)
|
||||
report['headset']['alias'] = 'living-room-frame'
|
||||
rows = list(csv.reader(io.StringIO(power.csv_samples(report))))
|
||||
self.assertEqual([row[5] for row in rows[1:]], ['living-room-frame'] * 3)
|
||||
|
||||
def test_completed_stream_preserves_zero_unknown_units_and_downloads(self):
|
||||
event = json.dumps({'sample': SAMPLE})
|
||||
program = f'import sys; sys.stdin.read(); print({event!r}); print(\'{{"finished": {{"stopped": null}}}}\')'
|
||||
|
||||
Reference in new issue
Block a user