Documentation
¶
Overview ¶
Package player plays immersive video on XR glasses.
It joins three things that know nothing about each other: a decoder (go-macos/avfoundation), the geometry of immersive video (go-xrkit/xrkit), and a window on the right physical display (go-widgets/window). What is left over — and what lives here — is the judgement: which display is the glasses, what shape the content is, and when to show each frame.
Index ¶
Constants ¶
const ( SeekStep = 10 * time.Second VolumeStep = 0.1 )
SeekStep and VolumeStep are how far one key press moves.
Variables ¶
var ErrUnsupported = errors.New("player: playback is implemented on macOS only")
ErrUnsupported is returned by Play on every platform without a decoder back-end. The geometry and display-choosing logic in this package is portable and tested everywhere; only playback is not.
Functions ¶
func Play ¶
Play reports ErrUnsupported. The Linux and Windows routes exist in principle -- VA-API or Media Foundation for decode, the X11/Wayland/Win32 back-ends of go-widgets/window for the display -- and are simply not written.
func ScalingAdvice ¶
ScalingAdvice returns a warning when a display is in a SCALED mode, or "" when there is nothing to say.
macOS offers displays a choice of modes, and a scaled one renders at a larger framebuffer and downsamples onto the panel. On a laptop screen that is a deliberate legibility trade. On XR glasses it is loss for nothing: the panel has one fixed native resolution, nobody is reading small text on it, and every downsample softens the image the user came for. A VITURE Beast was observed reporting "5120x1600 rendered, looks like 2560x800" while its panel is 3840x1080 -- three quarters of the rendered pixels thrown away.
The advice is only offered for displays that look like glasses. An external monitor in a scaled mode is the user's business.
func StereoMode ¶
StereoMode reports whether a display's dimensions look like a side-by-side 3D mode, and what one eye's viewport is.
XR glasses expose their 3D mode AS A DISPLAY MODE: the VITURE Beast reports 3840x1080 for side-by-side 3D and 1920x1200 for ordinary 2D. So there is no SDK involved in getting stereo out — only the question of which mode the glasses are in, which is what this answers. A panel wider than 21:9 is carrying two eyes; anything else is one.
Types ¶
type Action ¶
type Action int
Action is what a key press does during playback.
const ( // ActionNone is a key with no meaning here, which is most of them. ActionNone Action = iota // ActionQuit ends playback and closes the window. ActionQuit // ActionTogglePause stops and restarts the clock. ActionTogglePause // ActionSeekBack and ActionSeekForward move by [SeekStep]. ActionSeekBack ActionSeekForward // ActionVolumeDown and ActionVolumeUp move by [VolumeStep]. ActionVolumeDown ActionVolumeUp )
The actions a viewer can take. There is no on-screen control: the window is full screen with no chrome, so the keyboard is the whole interface and every binding has to be one someone would guess.
func KeyAction ¶
KeyAction maps a key name to what it does.
The names are the DOM-style ones the toolkit reports ("ArrowLeft", "Escape"), with printable keys arriving as themselves. Both the arrows and j/k/l are bound, because j/k/l is what every video player has taught people and the arrows are what everyone tries first — binding only one of them would be a preference imposed on a viewer wearing a headset who cannot see a help screen.
An unknown key does nothing. It deliberately does NOT quit: this used to end playback on ANY key, which meant brushing the keyboard closed the film.
type Config ¶
type Config struct {
// Path is the video file.
Path string
// Screen names the display to play on, matched case-insensitively against
// the attached displays. Empty picks automatically — recognised glasses
// first. See [ChooseDisplay].
Screen string
// Geometry overrides what the content is. Nil detects it from the file name
// and frame shape; see [Detect].
Geometry *Geometry
// FOVyDeg overrides the vertical field of view of each eye's view, in
// degrees. Zero -- the usual case -- takes the field the IDENTIFIED model
// publishes, per pair of glasses rather than per brand, and says so.
//
// It does not change how large flat material appears: the virtual screen
// is sized to fill the view, so the picture covers the same pixels
// whatever the field is. What it changes is the angle the player reports,
// and the framing of panoramic material, which a field of view really
// does frame.
FOVyDeg float64
// AudioDevice names the audio output to play the sound on, by its name or
// by its unique id. Empty -- the usual case -- plays on the glasses' own
// output when they publish one, and leaves the choice to the system when
// they do not.
//
// A device asked for by name and not found is NOT replaced by a guess: the
// player says so and lets the system choose, because silently playing
// somewhere else is the fault this setting exists to fix.
AudioDevice string
// Loop restarts at the end instead of stopping.
Loop bool
// Mono forces a single-eye view even on a display that looks stereoscopic.
// Useful for looking at the result on an ordinary monitor.
Mono bool
// Log, when set, receives one line of progress per notable event.
Log func(string)
// For, when positive, stops playback after this long. A full-screen window
// with no chrome is awkward to end from the outside, and an automated check
// cannot press a key at all.
For time.Duration
// Snapshot, when set, names a PNG to write once Frames have been shown. It
// captures the composed framebuffer -- the exact pixels sent to the glasses --
// which is the one form of visual proof that needs no screen-recording
// consent.
Snapshot string
// SnapshotAfter is how many frames to show before taking the snapshot.
// Zero means the first frame.
SnapshotAfter int
// Convert turns an ordinary FLAT film into 3D, frame by frame, by
// estimating how far away everything is and synthesising a second eye.
//
// It is ignored for a film that is already stereoscopic, and on a display
// showing one eye -- in both cases there is nothing to convert TO, and
// saying so beats doing expensive work that changes nothing.
//
// What it cannot do is invent what the camera never saw. Where a near
// object moves aside, what is behind it is guessed from the same row, so
// an edge is a little smeared. That is the honest cost of the effect.
Convert bool
// DepthModel names a Core ML depth model -- an .mlpackage or an already
// compiled .mlmodelc -- to estimate depth with. Empty falls back to an
// estimate made from cues in the picture itself, which needs nothing at
// all and is visibly not as good.
DepthModel string
// DepthCurve reshapes the depth before it becomes a sideways shift, as an
// S-curve of the given strength. Zero means none.
//
// It flattens both ends of the depth range and expands the middle, so the
// subject gains relief while the background and the foreground each settle
// into their own plane. It is NOT a comfort control: a near object gets
// slightly MORE disparity, not less.
//
// At the default disparity it barely matters -- twelve pixels each way is
// thirteen distinct shifts for the whole range, and quantisation swamps
// any reshaping of it. Raise Disparity before expecting to see this.
DepthCurve float64
// Disparity is how far apart the two eyes put the NEAREST thing, in pixels
// of the source. Zero uses 24.
//
// Small on purpose: a large disparity makes an impressive still and an
// unwatchable film, because the eyes must converge differently on every
// cut.
Disparity int
}
Config parametrises Play.
type Display ¶
type Display struct {
Name string
Width, Height int
Primary bool
// Scale is the display's backing factor: framebuffer pixels per logical
// point. 1 means the framebuffer matches the mode; 2 means macOS renders at
// twice the size and scales down.
Scale float64
}
Display is the subset of a window back-end's screen description this package needs. It is declared here, rather than taken from go-widgets/window, so the choosing logic can be tested against a list of displays that do not exist.
func ChooseDisplay ¶
ChooseDisplay picks the display to play on.
want, when not empty, is matched case-insensitively against the display names and must match exactly one — an ambiguous request is an error rather than a coin toss, because playing full screen on the wrong monitor takes over the machine the user was using.
With no preference the order is: a recognised pair of glasses; failing that the widest non-primary display, since an external panel is far more likely to be the intended target than the laptop the user is driving from; failing that the primary, so the thing still runs on one screen.
type Geometry ¶
type Geometry struct {
Projection projection.Projection
Format stereo.Format
// Why records how the guess was made, so a wrong one can be understood and
// overridden rather than just disbelieved.
Why string
}
Geometry is everything needed to reshape a frame: what the source depicts, and how it packs its eyes.
func Detect ¶
Detect guesses a file's geometry from its name and frame dimensions.
Guessing is unavoidable: none of this is reliably recorded in the container, and the conventions that exist are conventions of FILE NAMING. The guess is therefore explicit about its reasoning, and every part of it is overridable — a viewer that silently mis-detects 180-degree content as 360 shows the world squeezed into half the view and gives the user nothing to act on.
The order matters: an explicit marker in the name always beats a deduction from the aspect ratio, because a name was written by someone who knew.
type Rect ¶
type Rect struct{ X, Y, W, H int }
Rect is a rectangle in framebuffer pixels. It mirrors the toolkit's own shape so the layout can be computed and tested without a widget tree.
type SourceInfo ¶
type SourceInfo struct {
Width, Height int
FrameRate float64
Duration time.Duration
// Container names what the file is and which path opened it, for the log.
Container string
// Codec names the video codec, when the source knows it.
Codec string
}
SourceInfo describes the video a source yields.
