Reach the DWS on the port it is actually listening on

A BrightSign screenshot showed a card reading "Video is playing on the
hardware plane and cannot be captured" while the very same capture worked
perfectly from the player's own DWS Snapshots tab. The player was asking the
wrong port.

autorun.brs hardcoded http://localhost/api/v1/snapshot/ — port 80. The DWS port
is configurable and BSN/Supervisor-provisioned players are commonly moved off
it: the unit this was found on serves DWS on 8080 with nothing listening on 80
at all. Every host capture therefore failed to connect and fell through to the
in-page canvas, which cannot read the hardware video plane — so the fallback
produced an honest-sounding message about the video, and the actual fault (a
port) never appeared anywhere.

The port lives in the networking registry section as http_server, which is the
same place the DWS itself is configured from, so that is where this reads it.
80 remains the default when the key is absent.

Also 127.0.0.1 rather than "localhost": a name has to be resolved, and if that
resolution answers ::1 first the connection goes to an address the DWS is not
listening on. A literal cannot be resolved wrongly.

This is necessary but NOT sufficient — the capture still does not work on that
hardware, for an unrelated reason recorded in brightsign/README.md: the page
cannot reach the host at all after load, so the Sub that would use this URL is
never entered. Fixing the port anyway, because it would have broken the capture
a second time the moment the messaging problem is solved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014skWYXJUWhF73EvNPgB2AS
This commit is contained in:
ScreenTinker 2026-08-07 14:14:45 -05:00
parent 04f88d3340
commit f0f7f35103

View file

@ -100,6 +100,41 @@ Function LoadConfig() As Object
return cfg
End Function
Function SnapshotDir() As String
' DWS writes to ITS primary storage, which is not necessarily the volume the presentation
' booted from — so probe rather than assume, in the same order StorageRoot() does.
for each v in ["USB1:", "SSD:", "SD:", "SD2:", "FLASH:"]
d$ = v + "/remote_snapshots"
files = MatchFiles(d$, "*.jpg")
if files <> invalid and files.Count() > 0 then return d$
end for
return ""
End Function
Function NewestFile(dir As String, pattern As String) As String
' DWS names captures img-YYYY-MM-DD-HH-MM-SS.jpg, so the lexicographic maximum IS the newest.
best$ = ""
files = MatchFiles(dir, pattern)
if files = invalid then return ""
for each f in files
if f > best$ then best$ = f
end for
return best$
End Function
Function DwsPort() As String
' Which port the local Diagnostic Web Server answers on. Read from the same registry the
' DWS itself is configured from, so a player moved off port 80 still gets framebuffer
' captures instead of silently degrading to the canvas path.
port$ = "80"
reg = CreateObject("roRegistrySection", "networking")
if reg <> invalid and reg.Exists("http_server") then
v$ = reg.Read("http_server").Trim()
if v$ <> "" then port$ = v$
end if
return port$
End Function
Sub SaveRegistry(key As String, value As String)
reg = CreateObject("roRegistrySection", "screentinker")
reg.Write(key, value)
@ -218,56 +253,67 @@ Sub TakeSnapshot(widget As Object, req As Object)
return
end if
ut.SetUrl("http://localhost/api/v1/snapshot/")
' The DWS port is NOT always 80. It is configurable and BSN/Supervisor-provisioned players
' are commonly moved off it — the unit this was found on serves DWS on 8080 with nothing
' listening on 80 at all. Hardcoding 80 meant every host snapshot failed to connect, fell
' through to the in-page canvas, and the canvas cannot read the hardware video plane, so the
' operator got a card reading "Video is playing on the hardware plane and cannot be captured"
' while the very same capture worked perfectly from the DWS Snapshots tab.
'
' The port lives in the networking registry section as http_server; absent means the default.
' 127.0.0.1, NOT "localhost". A name has to be resolved, and on this platform that resolution
' is not ours to rely on: if it answers ::1 first the connection goes to an address the DWS is
' not listening on and the transfer sits there until something times out — which is exactly the
' shape of the failure this chased (the page gave up at 15s having heard nothing at all, not
' even this Sub's own timeout). A literal address cannot be resolved wrongly.
ut.SetUrl("http://127.0.0.1:" + DwsPort() + "/api/v1/snapshot/")
ut.SetUserAndPassword("admin", serial$)
ut.AddHeader("Content-Type", "application/json")
' PostFromStringWithRetry does not exist — calling it raised "Member function not found" from
' inside the event loop, i.e. a snapshot request took the whole player down. And the synchronous
' PostFromString() is no use either: it returns only a response CODE and discards the body, which
' is where the thumbnail is. The documented way to read a POST response is asynchronous, on a
' message port.
port = CreateObject("roMessagePort")
ut.SetPort(port)
if not ut.AsyncPostFromString(body$) then
widget.PostJSMessage({ type: "snapshot-result", ok: false, error: "could not reach the local DWS" })
' SYNCHRONOUS, deliberately — and this is the whole fix.
'
' PostFromStringWithRetry does not exist (calling it raised "Member function not found" from
' inside the event loop, i.e. a snapshot request took the whole player down). The obvious
' alternative, AsyncPostFromString + Wait on a private port, is the documented way to read a
' POST body — and on this hardware its roUrlEvent NEVER ARRIVES. The Sub simply sat in Wait
' while st-bridge.js gave up at 15s, so the page reported "host did not answer in time" and
' fell back to the canvas, which cannot read the hardware video plane. Every other transfer in
' this file is synchronous (GetToString for the package check, GetToFile for the download) and
' every one of them works, including the self-update that replaced this very script.
'
' PostFromString returns only the response CODE and discards the body — which would normally
' lose the thumbnail. It does not matter here: DWS WRITES THE CAPTURE TO PRIMARY STORAGE before
' it answers (the body carries a `filename` pointing at it), so the file is on disk by the time
' the call returns and can simply be read back.
code% = ut.PostFromString(body$)
if code% <> 200 then
widget.PostJSMessage({ type: "snapshot-result", ok: false, error: "DWS refused the capture (HTTP " + Stri(code%).Trim() + " on port " + DwsPort() + ")" })
return
end if
' Bounded: a capture that never answers must not wedge the event loop that drives playback.
ev = Wait(20000, port)
if type(ev) <> "roUrlEvent" then
ut.AsyncCancel()
widget.PostJSMessage({ type: "snapshot-result", ok: false, error: "the local DWS did not answer" })
dir$ = SnapshotDir()
if dir$ = "" then
widget.PostJSMessage({ type: "snapshot-result", ok: false, error: "DWS wrote no capture to any volume" })
return
end if
resp$ = ev.GetString()
if resp$ = "" then
widget.PostJSMessage({ type: "snapshot-result", ok: false, error: "no response from the local DWS" })
newest$ = NewestFile(dir$, "*.jpg")
if newest$ = "" then
widget.PostJSMessage({ type: "snapshot-result", ok: false, error: "no capture found in " + dir$ })
return
end if
json = ParseJson(resp$)
if json = invalid or json.data = invalid then
widget.PostJSMessage({ type: "snapshot-result", ok: false, error: "unparseable DWS response" })
ba2 = CreateObject("roByteArray")
if not ba2.ReadFile(dir$ + "/" + newest$) then
widget.PostJSMessage({ type: "snapshot-result", ok: false, error: "could not read " + newest$ })
return
end if
if json.data.error <> invalid then
' e.g. "No primary storage found." — pass the player's own words through; inventing a
' friendlier message here would hide the one fact that explains the failure.
widget.PostJSMessage({ type: "snapshot-result", ok: false, error: json.data.error.message })
return
end if
r = json.data.result
if r = invalid or r.remotesnapshotthumbnail = invalid then
widget.PostJSMessage({ type: "snapshot-result", ok: false, error: "DWS returned no thumbnail" })
return
end if
widget.PostJSMessage({ type: "snapshot-result", ok: true, image: r.remotesnapshotthumbnail })
' Read, hand over, then remove: DWS appends a new file per capture and nothing else prunes
' them, so a 1fps remote-control stream would otherwise fill the volume.
img$ = "data:image/jpeg;base64," + ba2.ToBase64String()
DeleteFile(dir$ + "/" + newest$)
widget.PostJSMessage({ type: "snapshot-result", ok: true, image: img$ })
End Sub
' Rotate the OUTPUT, not the DOM.