Audio sync rewritten. Pause fixed. Video decoding performance improvements. Ability to have disc-less games.

This commit is contained in:
Scott Duensing 2026-09-02 18:13:06 -05:00
parent f6d63b6eb0
commit 34d61d957a
11 changed files with 486 additions and 108 deletions

View file

@ -15,6 +15,15 @@ API Changes
LEGACY_SPRITE_ARGS = true to their games.dat entry. See "Migrating from
Singe 2.10" in the manual.
- Singe is no longer laserdisc only. A game declares a disc with
DISC = true in games.dat (or --disc / --framefile on the command line);
otherwise it runs without one, on a canvas sized by CANVAS_X/CANVAS_Y or
--canvas=WxH (default 720x480), the disc functions become harmless, and
discGetState() returns the new DISC_EJECTED.
Every existing games.dat entry needs DISC = true added; the menu skips
an entry with VIDEO but no DISC = true and says why, and the engine
refuses one if launched directly. Scripts can test SINGE_DISC.
- mouseSetEnabled(bool) and singeSetPauseKeyEnabled(bool) added. The old
mouseEnable/mouseDisable and singeEnablePauseKey/singeDisablePauseKey are
now aliases defined in Framework.singe.
@ -44,6 +53,13 @@ API Changes
pause, quit, screenshot, or grab now work in MODE_FULL too; keyboard
mappings stay raw there.
- Video decoding runs on its own thread per video, so a discSearch() far
from a keyframe no longer stalls the game loop; the previous frame stays
on screen until the new one is ready. The laserdisc and framefile videos
are handed to the GPU as YUV and converted there instead of on the CPU,
and each video reports its keyframe spacing when loaded, with a warning
when seeks may be slow.
- Constants that used to be duplicated in Framework.singe (SWITCH_*,
FONT_QUALITY_*, MODE_*, MOUSE_*, OVERLAY_*, RENDER_*, SOUND_ERROR_*) are
now defined by the engine. New: DISC_STOPPED/DISC_PLAYING/DISC_PAUSED

View file

@ -301,10 +301,20 @@ for dir in lfs.dir(".") do
for file in lfs.dir(dir .. "/.") do
if file == "games.dat" then
-- Load games.dat
GAMES = {}
dofile(dir .. "/games.dat")
for key,value in pairs(GAMES) do
table.insert(GAME_LIST, value)
GAME_COUNT = GAME_COUNT + 1
for _, value in pairs(GAMES or {}) do
-- Since 2.20 a laserdisc game must say DISC = true; refuse the ambiguous cases here
-- with a message rather than letting the engine stop the menu when it is picked.
local title = tostring(value.TITLE or value.SCRIPT or "?")
if value.VIDEO and not value.DISC then
debugPrint(dir .. "/games.dat: \"" .. title .. "\" names a VIDEO without DISC = true; add DISC = true to the entry. Skipped.")
elseif value.DISC and not value.VIDEO then
debugPrint(dir .. "/games.dat: \"" .. title .. "\" says DISC = true but has no VIDEO. Skipped.")
else
table.insert(GAME_LIST, value)
GAME_COUNT = GAME_COUNT + 1
end
end
GAMES = {}
end

View file

@ -107,15 +107,17 @@ Singe [OPTIONS] scriptName{.singe}
The script name is the only required argument. It may be a `.singe` file, or
a directory containing a script of the same name (`ActionMax` finds
`ActionMax/ActionMax.singe`). When no `--framefile` is given, Singe looks for
a video next to the script with the same base name and any extension FFmpeg
can demux, then for a `.txt` framefile.
`ActionMax/ActionMax.singe`). For a laserdisc game (`--disc`) with no
`--framefile`, Singe looks for a video next to the script with the same base
name and any extension FFmpeg can demux, then for a `.txt` framefile.
[cols="1,2",options="header"]
|===
| Option | Purpose
| `-a`, `--aspect=N:D` | Force the aspect ratio used to pick a window size (`4:3`, `16:9`, `16:10`).
| `-c`, `--showcalculated` | Print the frame ranges of every segment of a framefile, for debugging.
| `-C`, `--canvas=WxH` | World size for a game without a disc, default 720x480. Ignored when there is a disc.
| `-D`, `--disc` | Play a laserdisc video: the one named by `--framefile`, or the video found next to the script. Implied by `--framefile`. Without it a video next to the script is reported and ignored.
| `-d`, `--datadir=PATHNAME` | Directory for everything Singe writes: video indexes, `trace.txt`, screenshots, the menu's `menu.dat`. A subdirectory named for the game's directory is created inside it. Defaults to the game's own directory.
| `-e`, `--volume_nonvldp=PERCENT` | Sound effect and extra video volume, `0` to `100`.
| `-f`, `--fullscreen` | Exclusive full screen at the desktop resolution.
@ -533,6 +535,7 @@ GAMES = {
{
TITLE = ".38 Ambush Alley",
SCRIPT = "ActionMax/38AmbushAlley.singe",
DISC = true,
VIDEO = "ActionMax/frame_38AmbushAlley.txt",
DATA = "ActionMax",
STRETCH = false,
@ -558,14 +561,38 @@ GAMES = {
}
----
The keys `SCRIPT`, `VIDEO`, `STRETCH`, `NO_MOUSE`, `RESOLUTION_X`,
`RESOLUTION_Y`, `SINDEN_GUN`, `AUDIO_TRACK`, and `LEGACY_SPRITE_ARGS` are
read by the engine when the menu (or your own script, through
`scriptExecute` / `scriptPush`) launches the entry; they override the
command line. `LEGACY_SPRITE_ARGS = true` runs a game written for Singe 2.10
The keys `SCRIPT`, `DISC`, `VIDEO`, `CANVAS_X`, `CANVAS_Y`, `STRETCH`,
`NO_MOUSE`, `RESOLUTION_X`, `RESOLUTION_Y`, `SINDEN_GUN`, `AUDIO_TRACK`, and
`LEGACY_SPRITE_ARGS` are read by the engine when the menu (or your own
script, through `scriptExecute` / `scriptPush`) launches the entry; they
override the command line. A laserdisc game must say `DISC = true` and name
its `VIDEO`; an entry with a `VIDEO` but no `DISC = true` is refused with a
message, as is `DISC = true` without a `VIDEO`. A game without a disc leaves
both out and may set `CANVAS_X` / `CANVAS_Y` (default 720x480); see
<<withoutadisc,Games Without a Disc>>. `LEGACY_SPRITE_ARGS = true` runs a game written for Singe 2.10
with the old sprite argument order (see <<migrating,Migrating from Singe
2.10>>). The remaining keys are read by the menu for display.
[#withoutadisc]
=== Games Without a Disc
Singe no longer assumes a laserdisc. A game is a laserdisc game only when it
says so: `DISC = true` in its `games.dat` entry, or `--disc` (or a
`--framefile`) on the command line. Everything else runs without a disc, and
the world is a *canvas* instead of a video frame: `CANVAS_X` / `CANVAS_Y` in
`games.dat` or `--canvas=WxH` on the command line, 720x480 by default so a
discless game looks like a standard definition laserdisc game unless it
asks otherwise.
Without a disc the engine draws on black, the overlay defaults to half the
canvas as usual, and the `disc*` and `vldp*` functions become harmless:
`discGetState` reports `DISC_EJECTED`, `discGetFrame` returns `0`,
`discGetWidth` / `discGetHeight` return the canvas, `vldpGetPixel` returns
black, and the transport calls trace and do nothing. Everything else -- the
overlay, sprites, fonts, sounds, extra videos through `videoLoad`, input,
and both programming models -- is identical. The global `SINGE_DISC` tells a
script which kind of game it is running as.
[#migrating]
=== Migrating from Singe 2.10
@ -608,6 +635,11 @@ changed shape, but a few behaviors did:
* `mouseEnable` / `mouseDisable` and `singeEnablePauseKey` /
`singeDisablePauseKey` still work, as aliases of `mouseSetEnabled` and
`singeSetPauseKeyEnabled`.
* A laserdisc game must now declare itself: add `DISC = true` next to
`VIDEO` in every `games.dat` entry, and pass `--disc` (or `--framefile`)
on the command line. Without it the game runs without a disc, and a
`games.dat` entry that names a `VIDEO` without `DISC = true` is refused
with a message naming the fix. See <<withoutadisc,Games Without a Disc>>.
* The pause key now pauses the whole game, not just the media: the script
is frozen until the key is pressed again, and `SWITCH_PAUSE` is delivered
to the script only when the key has been disabled. It acts on the key
@ -627,11 +659,12 @@ available to `controls.cfg` and to `Framework.singe` alike:
| `MOUSE_SINGLE`, `MOUSE_MANY` (also `SINGLE_MOUSE`, `MANY_MOUSE`) | Arguments for `mouseSetMode`.
| `OVERLAY_NOT_UPDATED`, `OVERLAY_UPDATED` | Return values for `onOverlayUpdate`.
| `RENDER_PIXELATED`, `RENDER_SMOOTH` | Arguments for `spriteQuality` / `videoQuality`.
| `DISC_STOPPED`, `DISC_PLAYING`, `DISC_PAUSED` | Return values of `discGetState`.
| `DISC_STOPPED`, `DISC_PLAYING`, `DISC_PAUSED`, `DISC_EJECTED` | Return values of `discGetState`.
| `SOUND_ERROR_INVALID`, `SOUND_REMOVE_HANDLE` | `-1`, what `soundPlay` returns when no channel is free.
| `SINGE_VERSION_MAJOR`, `SINGE_VERSION_MINOR`, `SINGE_VERSION_STRING`, `SINGE_FRAMEWORK_VERSION` | The engine version, as integers, as a string (`"v2.20"`), and as the number `singeVersion()` returns.
| `SINGE_DEAD_ZONE` | The `DEAD_ZONE` from `controls.cfg`.
| `SINGE_LEGACY_SPRITE_ARGS` | True when the game asked for the 2.10 sprite argument order.
| `SINGE_DISC` | True when the game has a laserdisc; false when the canvas is the world.
| `SINGE_GAMEPAD_BASE`, `SINGE_GAMEPAD_STRIDE`, `SINGE_AXIS_STRIDE`, `SINGE_GAMEPAD_BUTTON_OFFSET`, `SINGE_MOUSE_BASE`, `SINGE_MOUSE_STRIDE`, `SINGE_MAX_CONTROLLERS`, `SINGE_MAX_MICE` | Layout of the controller and mouse input codes; `Framework.singe` builds the `GAMEPAD_N` and `MOUSE_N` tables from them.
|===
@ -671,7 +704,12 @@ format, channel count, and rate.
The first time a video is opened, Singe indexes it and stores the index next
to the game's other data (`<name>.index`). Indexing takes a while for large
files and happens again if the video changes.
files and happens again if the video changes. When a video is loaded, Singe
reports its keyframe spacing in the program trace and prints a warning if
keyframes are more than two seconds apart, because a seek has to decode
forward from the previous keyframe. Decoding happens on a separate thread,
so a slow seek shows the previous frame a little longer instead of stalling
the game.
For laserdisc footage the constraints are frame accuracy and seek speed, not
compression. H.264 in MP4 or MKV with a short keyframe interval (one or two
@ -1045,6 +1083,7 @@ Returns an integer describing the playback state of the disc.
| `DISC_STOPPED` | `2` | Stopped (`discStop` was called)
| `DISC_PLAYING` | `3` | Playing
| `DISC_PAUSED` | `4` | Paused
| `DISC_EJECTED` | `5` | The game has no disc (see <<withoutadisc,Games Without a Disc>>)
|===
Singe's lightweight player has no distinct searching state; a disc that is seeking reports `DISC_PAUSED`.

View file

@ -13,6 +13,14 @@ ActionMax/Emulator.singe
lengthIntro, lengthGame, lengthMenu, highThreshold, lowThreshold,
sensorX, sensorY, sensorLeft, and sensorTop before loading it.
The ActionMax games.dat is not part of this repository. Since Singe
2.20 every laserdisc entry in it needs one added line, next to VIDEO:
DISC = true,
Entries without it are skipped by the menu with a message, and an entry
that names a VIDEO without DISC = true is refused by the engine.
daitarn_3_singe/Script/toolbox.singe
Helper library from "Daitarn 3" (Karis, 2020). The calling script must
define OVLW, OVLH, and bPause.

View file

@ -216,7 +216,7 @@ int32_t frameFileLoad(const char *filename, const char *indexPath, SDL_Renderer
audio = NULL;
}
}
files[count].videoHandle = videoLoad(files[count].filename, audio, indexPath, renderer);
files[count].videoHandle = videoLoad(files[count].filename, audio, indexPath, renderer, false);
free(audio);
count++;
}

View file

@ -124,6 +124,8 @@ static const OptionT _options[] = {
{ 'a', "aspect", ap_yes, "N:D", "force aspect ratio", false },
{ 'b', "scalefactor", ap_yes, "PERCENT", "reduce screen size for overscan compensation", true },
{ 'c', "showcalculated", ap_no, NULL, "show calculated framefile values for debugging", false },
{ 'C', "canvas", ap_yes, "WxH", "world size for games without a disc (default 720x480)", false },
{ 'D', "disc", ap_no, NULL, "play a laserdisc video (implied by --framefile)", false },
{ 'd', "datadir", ap_yes, "PATHNAME", "alternate location for written files", false },
{ 'e', "volume_nonvldp", ap_yes, "PERCENT", "specify sound effects volume in percent", false },
{ 'f', "fullscreen", ap_no, NULL, "run in full screen mode", false },
@ -540,6 +542,7 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[])
int32_t aspectDom = -1;
int32_t *target = NULL;
char *aspectString = NULL;
char *canvasString = NULL;
char *sindenString = NULL;
char *temp = NULL;
const char *arg = NULL;
@ -574,6 +577,8 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[])
conf->volumeNonVldp = VOLUME_MAX;
conf->scaleFactor = SCALE_FACTOR_MAX;
conf->resolutionWasCalculated = true;
conf->canvasWidth = CANVAS_DEFAULT_WIDTH;
conf->canvasHeight = CANVAS_DEFAULT_HEIGHT;
// Parse command line
for (argIndex = 0; argIndex < ap_arguments(&parser); argIndex++) {
@ -608,6 +613,17 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[])
conf->showCalculated = true;
break;
// Canvas size
case 'C':
free(canvasString);
canvasString = strdup(arg);
break;
// Laserdisc
case 'D':
conf->disc = true;
break;
// Data Dir
case 'd':
free(conf->dataDir);
@ -680,10 +696,11 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[])
conf->stretchVideo = true;
break;
// Video File
// Video File - a video means a disc.
case 'v':
free(conf->videoFile);
conf->videoFile = strdup(arg);
conf->disc = true;
break;
// Full Screen Windowed
@ -778,6 +795,19 @@ static ConfigT *_parseArguments(const char *exeName, int32_t argc, char *argv[])
free(aspectString);
}
// Did they specify a canvas size?
if (canvasString) {
temp = strchr(canvasString, 'x');
if (temp == NULL) {
_showUsage(exeName, "Canvas size must be WIDTHxHEIGHT, for example 640x480.");
}
*temp = 0;
if (!_parseInteger(canvasString, &conf->canvasWidth) || !_parseInteger(temp + 1, &conf->canvasHeight) || (conf->canvasWidth <= 0) || (conf->canvasHeight <= 0)) {
_showUsage(exeName, "Canvas size must be WIDTHxHEIGHT, for example 640x480.");
}
free(canvasString);
}
return conf;
}
@ -849,11 +879,17 @@ static void _resolveFiles(const char *exeName, ConfigT *conf) {
}
}
free(temp);
// A game is only a laserdisc game when it says so.
if (!conf->disc && conf->videoFile) {
utilSay("Note: %s found but --disc was not given; running without a disc.", conf->videoFile);
free(conf->videoFile);
conf->videoFile = NULL;
}
}
if (!conf->videoFile) {
if (conf->disc && !conf->videoFile) {
_showUsage(exeName, "Unable to locate video.");
}
conf->isFrameFile = isFrameFileName(conf->videoFile);
conf->isFrameFile = conf->disc && isFrameFileName(conf->videoFile);
if (conf->dataDir) {
// They provided a data directory. Append the game name.

View file

@ -33,6 +33,8 @@
#define SCALE_FACTOR_MIN 50
#define SCALE_FACTOR_MAX 100
#define DIRECTORY_MODE 0777
#define CANVAS_DEFAULT_WIDTH 720
#define CANVAS_DEFAULT_HEIGHT 480
ConfigT *cloneConf(const ConfigT *conf);

View file

@ -139,7 +139,8 @@ typedef enum MouseModeE {
typedef enum DiscStateE {
DISC_STOPPED = 2,
DISC_PLAYING = 3,
DISC_PAUSED = 4
DISC_PAUSED = 4,
DISC_EJECTED = 5 // The game has no disc at all
} DiscStateE;
typedef enum FontQualityE {
@ -290,7 +291,9 @@ typedef struct GlobalS {
int32_t keyboardLastDown;
int32_t keyboardLastUp;
int32_t frameFileHandle;
int32_t videoHandle;
int32_t videoHandle; // -1 when the game has no disc
int32_t canvasWidth; // World size: the disc's, or the configured canvas
int32_t canvasHeight;
FontQualityE fontQuality;
MouseModeE mouseMode;
int32_t mouseCount;
@ -718,8 +721,12 @@ static ConfigT *_buildConfFromTable(lua_State *L) {
int64_t valueNumber = 0;
ConfigT *c = NULL;
// Start with current config.
// Start with current config, but every entry declares its own disc.
c = cloneConf(_global.conf);
c->disc = false;
c->isFrameFile = false;
free(c->videoFile);
c->videoFile = NULL;
// Update with data in the table on the top of the Lua stack.
lua_pushnil(L);
@ -784,12 +791,29 @@ static ConfigT *_buildConfFromTable(lua_State *L) {
c->audioOutputTrack = (int32_t)valueNumber;
} else if (strcmp(confKey, "LEGACY_SPRITE_ARGS") == 0) {
c->legacySpriteArgs = valueBoolean;
} else if (strcmp(confKey, "DISC") == 0) {
c->disc = valueBoolean;
} else if (strcmp(confKey, "CANVAS_X") == 0) {
c->canvasWidth = (int32_t)valueNumber;
} else if (strcmp(confKey, "CANVAS_Y") == 0) {
c->canvasHeight = (int32_t)valueNumber;
}
// Clean up for next pair
lua_pop(L, 1);
}
// A laserdisc game says DISC = true and names its VIDEO; anything else is a mistake worth stopping for.
if (c->disc && (c->videoFile == NULL)) {
utilDie("%s: DISC = true but no VIDEO given.", c->scriptFile);
}
if (!c->disc && (c->videoFile != NULL)) {
utilDie("%s: VIDEO given without DISC = true. Add DISC = true to the games.dat entry for a laserdisc game.", c->scriptFile);
}
if ((c->canvasWidth <= 0) || (c->canvasHeight <= 0)) {
utilDie("%s: CANVAS_X and CANVAS_Y must be positive.", c->scriptFile);
}
// Create new data dir location based on script location.
free(c->dataDir);
c->dataDir = createDataDir(c->dataDirBase, c->scriptFile);
@ -1591,6 +1615,8 @@ static void _pushConstants(lua_State *L) {
lua_setglobal(L, "DISC_PLAYING");
lua_pushinteger(L, DISC_PAUSED);
lua_setglobal(L, "DISC_PAUSED");
lua_pushinteger(L, DISC_EJECTED);
lua_setglobal(L, "DISC_EJECTED");
lua_pushinteger(L, -1);
lua_setglobal(L, "SOUND_ERROR_INVALID");
@ -1628,6 +1654,8 @@ static void _pushConstants(lua_State *L) {
lua_setglobal(L, "SINGE_DEAD_ZONE");
lua_pushboolean(L, _global.conf->legacySpriteArgs);
lua_setglobal(L, "SINGE_LEGACY_SPRITE_ARGS");
lua_pushboolean(L, _global.conf->disc);
lua_setglobal(L, "SINGE_DISC");
}
@ -1918,7 +1946,7 @@ static void _takeScreenshot(void) {
static void _updatePauseState(void) {
if (_global.pauseState) {
// Pause laserdisc
if (!_global.discStopped && videoIsPlaying(_global.videoHandle)) {
if ((_global.videoHandle >= 0) && !_global.discStopped && videoIsPlaying(_global.videoHandle)) {
_global.wasPlayingBeforePause = true;
videoPause(_global.videoHandle);
}
@ -1926,7 +1954,7 @@ static void _updatePauseState(void) {
Mix_Pause(-1);
} else {
// Resume laserdisc
if (!_global.discStopped && _global.wasPlayingBeforePause) {
if ((_global.videoHandle >= 0) && !_global.discStopped && _global.wasPlayingBeforePause) {
_global.wasPlayingBeforePause = false;
videoPlay(_global.videoHandle);
}
@ -2110,9 +2138,7 @@ static int32_t apiDiscGetFrame(lua_State *L) {
static int32_t apiDiscGetHeight(lua_State *L) {
int32_t height = 0;
if (_global.videoHandle >= 0) {
height = videoGetHeight(_global.videoHandle);
}
height = _global.canvasHeight;
_luaTrace(L, "discGetHeight", "%d", height);
lua_pushinteger(L, height);
@ -2140,11 +2166,13 @@ static int32_t apiDiscGetLanguage(lua_State *L) {
}
// state = discGetState() One of DISC_STOPPED, DISC_PLAYING, DISC_PAUSED.
// state = discGetState() One of DISC_STOPPED, DISC_PLAYING, DISC_PAUSED, or DISC_EJECTED (no disc).
static int32_t apiDiscGetState(lua_State *L) {
DiscStateE state = DISC_PAUSED;
if (_global.discStopped) {
if (_global.videoHandle < 0) {
state = DISC_EJECTED;
} else if (_global.discStopped) {
state = DISC_STOPPED;
} else {
if ((_global.videoHandle >= 0) && videoIsPlaying(_global.videoHandle)) {
@ -2162,9 +2190,7 @@ static int32_t apiDiscGetState(lua_State *L) {
static int32_t apiDiscGetWidth(lua_State *L) {
int32_t width = 0;
if (_global.videoHandle >= 0) {
width = videoGetWidth(_global.videoHandle);
}
width = _global.canvasWidth;
_luaTrace(L, "discGetWidth", "%d", width);
lua_pushinteger(L, width);
@ -2897,10 +2923,8 @@ static int32_t apiOverlaySetResolution(lua_State *L) {
utilDie("%s", SDL_GetError());
}
SDL_SetTextureBlendMode(_global.overlayTexture, SDL_BLENDMODE_BLEND);
if (_global.videoHandle >= 0) {
_global.overlayScaleX = (double)width / (double)videoGetWidth(_global.videoHandle);
_global.overlayScaleY = (double)height / (double)videoGetHeight(_global.videoHandle);
}
_global.overlayScaleX = (double)width / (double)_global.canvasWidth;
_global.overlayScaleY = (double)height / (double)_global.canvasHeight;
_overlayTouched();
_luaTrace(L, "overlaySetResolution", "%d %d", width, height);
@ -3827,7 +3851,7 @@ static int32_t apiVideoLoad(lua_State *L) {
if (!video) {
_luaDie(L, "videoLoad", "Unable to allocate new video.");
}
video->handle = videoLoad(name, NULL, dataDir, _global.renderer);
video->handle = videoLoad(name, NULL, dataDir, _global.renderer, true);
video->id = _global.nextVideoId++;
video->lastFrame = -1;
video->scaleX = 1.0;
@ -4327,21 +4351,29 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) {
lua_register(_global.luaContext, "vldpGetWidth", apiDiscGetWidth); // 1.xx Same as discGetWidth.
lua_register(_global.luaContext, "vldpSetVerbose", apiVldpSetVerbose); // 1.xx
// Open main video file
_progTrace("Opening main video file");
_doIndexDisplay(INDEX_DISPLAY_START);
videoSetIndexCallback(_doIndexDisplay);
if (_global.conf->isFrameFile) {
_global.frameFileHandle = frameFileLoad(_global.conf->videoFile, _global.conf->dataDir, _global.renderer, _global.conf->showCalculated);
frameFileSeek(_global.frameFileHandle, 0, &_global.videoHandle, &thisFrame); // Fills in _global.videoHandle
// Open main video file, if this is a laserdisc game. Otherwise the canvas is the world.
if (_global.conf->disc) {
_progTrace("Opening main video file");
_doIndexDisplay(INDEX_DISPLAY_START);
videoSetIndexCallback(_doIndexDisplay);
if (_global.conf->isFrameFile) {
_global.frameFileHandle = frameFileLoad(_global.conf->videoFile, _global.conf->dataDir, _global.renderer, _global.conf->showCalculated);
frameFileSeek(_global.frameFileHandle, 0, &_global.videoHandle, &thisFrame); // Fills in _global.videoHandle
} else {
_global.videoHandle = videoLoad(_global.conf->videoFile, NULL, _global.conf->dataDir, _global.renderer, false);
}
videoSetVolume(_global.videoHandle, _global.conf->volumeVldp, _global.conf->volumeVldp);
videoSetIndexCallback(NULL);
_doIndexDisplay(INDEX_DISPLAY_STOP);
_global.canvasWidth = videoGetWidth(_global.videoHandle);
_global.canvasHeight = videoGetHeight(_global.videoHandle);
} else {
_global.videoHandle = videoLoad(_global.conf->videoFile, NULL, _global.conf->dataDir, _global.renderer);
_progTrace("No disc; canvas is %dx%d", _global.conf->canvasWidth, _global.conf->canvasHeight);
_global.canvasWidth = _global.conf->canvasWidth;
_global.canvasHeight = _global.conf->canvasHeight;
}
videoSetVolume(_global.videoHandle, _global.conf->volumeVldp, _global.conf->volumeVldp);
videoSetIndexCallback(NULL);
_doIndexDisplay(INDEX_DISPLAY_STOP);
videoWidth = videoGetWidth(_global.videoHandle);
videoHeight = videoGetHeight(_global.videoHandle);
videoWidth = _global.canvasWidth;
videoHeight = _global.canvasHeight;
// Should we resize the window to the video's shape?
if (conf->resolutionWasCalculated && !conf->fullScreen && !conf->fullScreenWindow) {
@ -4507,11 +4539,13 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) {
SDL_SetColorKey(_global.consoleFontSurface, SDL_TRUE, _global.consoleFontSurface->format->Rmask | _global.consoleFontSurface->format->Bmask);
// The disc always starts parked on frame 1, paused, like discSearch(1).
_progTrace("Parking laserdisc on frame 1");
_discSeek(1);
videoPause(_global.videoHandle);
_global.discStopped = false;
_selectDefaultAudioTrack(_global.videoHandle);
if (_global.videoHandle >= 0) {
_progTrace("Parking laserdisc on frame 1");
_discSeek(1);
videoPause(_global.videoHandle);
_global.discStopped = false;
_selectDefaultAudioTrack(_global.videoHandle);
}
// Start script
_progTrace("Running %s", _global.conf->scriptFile);
@ -4696,16 +4730,18 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) {
}
}
// Update video
thisFrame = videoUpdate(_global.videoHandle, &_global.videoTexture);
if (_global.conf->isFrameFile) {
frameFileUpdate(_global.frameFileHandle, &_global.videoHandle);
}
// Did we get a new video frame?
if (thisFrame != lastFrame) {
lastFrame = thisFrame;
frameClock = 0;
_global.refreshDisplay = true;
// Update the disc video
if (_global.videoHandle >= 0) {
thisFrame = videoUpdate(_global.videoHandle, &_global.videoTexture);
if (_global.conf->isFrameFile) {
frameFileUpdate(_global.frameFileHandle, &_global.videoHandle);
}
// Did we get a new video frame?
if (thisFrame != lastFrame) {
lastFrame = thisFrame;
frameClock = 0;
_global.refreshDisplay = true;
}
}
// Call game code, unless the engine has it paused.
@ -4735,13 +4771,15 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) {
SDL_SetRenderDrawColor(_global.renderer, sindenWhiteColor.r, sindenWhiteColor.g, sindenWhiteColor.b, sindenWhiteColor.a);
SDL_RenderFillRect(_global.renderer, &sindenWhite);
}
// Laserdisc Video
if (_global.discStopped) {
// Stopped discs display blue like the good old days
SDL_SetRenderDrawColor(_global.renderer, 0, 0, BLUE_SCREEN_BLUE, SDL_ALPHA_OPAQUE);
SDL_RenderFillRect(_global.renderer, &windowTarget);
} else {
SDL_RenderCopy(_global.renderer, _global.videoTexture, NULL, &windowTarget);
// Laserdisc Video. Games without a disc draw on black.
if (_global.videoHandle >= 0) {
if (_global.discStopped) {
// Stopped discs display blue like the good old days
SDL_SetRenderDrawColor(_global.renderer, 0, 0, BLUE_SCREEN_BLUE, SDL_ALPHA_OPAQUE);
SDL_RenderFillRect(_global.renderer, &windowTarget);
} else {
SDL_RenderCopy(_global.renderer, _global.videoTexture, NULL, &windowTarget);
}
}
// Overlay
if (_global.overlayDirty) {
@ -4806,11 +4844,14 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) {
}
// Unload background video
_progTrace("Unloading main video file");
if (_global.conf->isFrameFile) {
_progTrace("Unloading framefile");
frameFileUnload(_global.frameFileHandle);
} else {
videoUnload(_global.videoHandle);
if (_global.videoHandle >= 0) {
_progTrace("Unloading main video file");
videoUnload(_global.videoHandle);
}
}
// Stop controllers

View file

@ -63,12 +63,15 @@ typedef struct ConfigS {
bool programTracing;
bool scriptTracing;
bool legacySpriteArgs;
bool disc; // Play a laserdisc video; otherwise the canvas is the world
int32_t bestRatioIndex;
int32_t volumeVldp;
int32_t volumeNonVldp;
int32_t scaleFactor;
int32_t xResolution;
int32_t yResolution;
int32_t canvasWidth; // World size when there is no disc
int32_t canvasHeight;
int32_t sindenArgc;
int32_t sindenArgv[SINDEN_ARG_MAX];
int32_t audioOutputTrack;

View file

@ -55,8 +55,17 @@ typedef struct iso639_lang_t iso639_lang_t;
#define MS_PER_SECOND 1000.0
#define PERCENT_TO_SCALE 0.01f
#define PERCENT_MAX 100
#define PLANE_COUNT 3 // Y, U, V
#define KEYFRAME_WARN_SECONDS 2.0 // Seeks decode forward from the previous keyframe
// One decoded frame owned by the player (FFMS2 reuses its own buffer, so we copy).
typedef struct FrameBufferS {
uint8_t *data[PLANE_COUNT];
int32_t linesize[PLANE_COUNT];
int64_t frame; // -1 until something has been decoded into it
} FrameBufferT;
typedef struct AudioStreamS {
FFMS_AudioSource *audioSource;
const FFMS_AudioProperties *audioProps;
@ -69,7 +78,6 @@ typedef struct VideoPlayerS {
int32_t id;
bool playing;
bool resetTime;
bool frameDirty;
uint8_t *audioBuffer;
uint8_t *audioSilenceRaw;
uint8_t audioSampleBytes;
@ -102,21 +110,40 @@ typedef struct VideoPlayerS {
FFMS_Track *videoTrackHandle;
const FFMS_VideoProperties *videoProps;
const FFMS_TrackTimeBase *videoTimeBase;
const FFMS_Frame *frameData;
bool rgb; // BGRA frames for scripts to read; otherwise YUV for the GPU
FrameBufferT front; // What the texture and the script see
FrameBufferT back; // What the decoder thread fills
bool backReady;
bool threadError;
bool quitThread;
int64_t requestedFrame; // Waiting for the decoder, -1 when none
int64_t pendingFrame; // Last frame asked for, so it is not asked for twice
int64_t uploadedFrame; // Frame currently on the texture
SDL_Thread *thread;
SDL_mutex *lock;
SDL_cond *wake;
FFMS_ErrorInfo threadErrInfo;
char threadErrMsg[ERROR_BUFFER_SIZE];
UT_hash_handle hh;
} VideoPlayerT;
#pragma GCC diagnostic pop
static int64_t _audioClock(VideoPlayerT *v, uint32_t now);
static void _allocateFrameBuffer(VideoPlayerT *v, FrameBufferT *buffer);
static void _copyFrame(VideoPlayerT *v, FrameBufferT *buffer, const FFMS_Frame *frame);
static FFMS_Index *_createIndex(const char *filename, const char *indexPath, bool hasVideo, bool hasAudio, VideoPlayerT *v);
static int _decoderThread(void *data); // SDL thread entry. Not changing int.
static void _dequeueVideoAudio(int channel, void *stream, int bytes, void *udata); // Callback. Not changing ints.
static void _feedAudio(VideoPlayerT *v);
static int64_t _frameTime(VideoPlayerT *v, int64_t frame);
static VideoPlayerT *_getPlayer(int32_t playerHandle, const char *caller);
static int FFMS_CC _indexCallBack(int64_t current, int64_t total, void *icPrivate); // Callback. Not changing int.
static void _loadAudio(VideoPlayerT *v, const char *filename, FFMS_Index *index);
static void _loadFrame(VideoPlayerT *v);
static void _reportKeyframes(VideoPlayerT *v, const char *filename);
static void _requestFrame(VideoPlayerT *v);
static bool _takeDecodedFrame(VideoPlayerT *v);
static void _uploadFrame(VideoPlayerT *v);
static int64_t _msToSamples(VideoPlayerT *v, int64_t ms);
static void _resetClock(VideoPlayerT *v, uint32_t now);
@ -131,6 +158,49 @@ static int64_t _mixLatencyMs = 0; // Time between handing
static SDL_AudioFormat _mixFormat = 0;
static void _allocateFrameBuffer(VideoPlayerT *v, FrameBufferT *buffer) {
int32_t chromaWidth = (v->width + 1) / 2;
int32_t chromaHeight = (v->height + 1) / 2;
buffer->frame = -1;
if (v->rgb) {
buffer->linesize[0] = v->width * BYTES_PER_PIXEL;
buffer->data[0] = malloc((size_t)buffer->linesize[0] * (size_t)v->height);
if (!buffer->data[0]) {
utilDie("Unable to allocate frame buffer.");
}
} else {
buffer->linesize[0] = v->width;
buffer->linesize[1] = chromaWidth;
buffer->linesize[2] = chromaWidth;
buffer->data[0] = malloc((size_t)v->width * (size_t)v->height);
buffer->data[1] = malloc((size_t)chromaWidth * (size_t)chromaHeight);
buffer->data[2] = malloc((size_t)chromaWidth * (size_t)chromaHeight);
if (!buffer->data[0] || !buffer->data[1] || !buffer->data[2]) {
utilDie("Unable to allocate frame buffer.");
}
}
}
// Copies FFMS2's frame into our buffer, one plane at a time (line sizes may differ).
static void _copyFrame(VideoPlayerT *v, FrameBufferT *buffer, const FFMS_Frame *frame) {
int32_t plane = 0;
int32_t planes = v->rgb ? 1 : PLANE_COUNT;
int32_t rows = 0;
int32_t width = 0;
int32_t y = 0;
for (plane = 0; plane < planes; plane++) {
rows = (plane == 0) ? v->height : (v->height + 1) / 2;
width = buffer->linesize[plane];
for (y = 0; y < rows; y++) {
memcpy(buffer->data[plane] + y * buffer->linesize[plane], frame->Data[plane] + y * frame->Linesize[plane], (size_t)width);
}
}
}
// Presentation time (ms) the listener is hearing right now. Audio is the master clock:
// the picture is fitted to what the device has actually consumed, so the two cannot drift.
static int64_t _audioClock(VideoPlayerT *v, uint32_t now) {
@ -200,6 +270,43 @@ static FFMS_Index *_createIndex(const char *filename, const char *indexPath, boo
}
// Decodes whatever frame was last requested into the back buffer, then waits for the next request.
// FFMS2 seeks by decoding forward from a keyframe, so this keeps long seeks off the main loop.
static int _decoderThread(void *data) {
VideoPlayerT *v = (VideoPlayerT *)data;
const FFMS_Frame *frame = NULL;
int64_t want = -1;
SDL_LockMutex(v->lock);
while (!v->quitThread) {
// Only one frame is in flight and the main thread must take it before the next one.
if ((v->requestedFrame < 0) || v->backReady) {
SDL_CondWait(v->wake, v->lock);
continue;
}
want = v->requestedFrame;
v->requestedFrame = -1;
SDL_UnlockMutex(v->lock);
frame = FFMS_GetFrame(v->videoSource, (int)want, &v->threadErrInfo);
if (frame != NULL) {
_copyFrame(v, &v->back, frame);
}
SDL_LockMutex(v->lock);
if (frame == NULL) {
v->threadError = true;
} else {
v->back.frame = want;
v->backReady = true;
}
}
SDL_UnlockMutex(v->lock);
return 0;
}
// Runs on the SDL_mixer audio thread. Everything it touches is guarded by SDL_LockAudio on the main thread.
static void _dequeueVideoAudio(int channel, void *stream, int bytes, void *udata) {
VideoPlayerT *v = (VideoPlayerT *)udata;
@ -375,19 +482,50 @@ static void _loadAudio(VideoPlayerT *v, const char *filename, FFMS_Index *index)
}
// Decode the current frame and push it to the texture.
static void _loadFrame(VideoPlayerT *v) {
v->frameData = FFMS_GetFrame(v->videoSource, (int)v->frame, &v->errInfo);
if (v->frameData == NULL) {
utilDie("%s", v->errInfo.Buffer);
}
SDL_UpdateTexture(v->videoTexture, NULL, v->frameData->Data[0], v->frameData->Linesize[0]);
v->frameDirty = false;
static int64_t _msToSamples(VideoPlayerT *v, int64_t ms) {
return (int64_t)((double)ms / MS_PER_SECOND * (double)v->audio[v->currentAudioTrack].audioProps->SampleRate);
}
static int64_t _msToSamples(VideoPlayerT *v, int64_t ms) {
return (int64_t)((double)ms / MS_PER_SECOND * (double)v->audio[v->currentAudioTrack].audioProps->SampleRate);
// Tells the author how far a seek may have to decode. Keyframe spacing is a property of the file.
static void _reportKeyframes(VideoPlayerT *v, const char *filename) {
int32_t frame = 0;
int32_t lastKeyframe = 0;
int32_t keyframes = 0;
int32_t longestGap = 0;
double seconds = 0.0;
for (frame = 0; frame < v->videoProps->NumFrames; frame++) {
if (FFMS_GetFrameInfo(v->videoTrackHandle, frame)->KeyFrame) {
if ((keyframes > 0) && (frame - lastKeyframe > longestGap)) {
longestGap = frame - lastKeyframe;
}
lastKeyframe = frame;
keyframes++;
}
}
if (v->videoProps->NumFrames - lastKeyframe > longestGap) {
longestGap = v->videoProps->NumFrames - lastKeyframe;
}
if (v->videoProps->FPSNumerator > 0) {
seconds = (double)longestGap * (double)v->videoProps->FPSDenominator / (double)v->videoProps->FPSNumerator;
}
utilTrace("%s: %d frames, %d keyframes, longest gap %d frames (%.1f seconds)", filename, v->videoProps->NumFrames, keyframes, longestGap, seconds);
if (seconds > KEYFRAME_WARN_SECONDS) {
utilSay("Warning: %s has keyframes up to %.1f seconds apart; seeking into that video may stall. Re-encode with a keyframe interval of two seconds or less.", filename, seconds);
}
}
// Asks the decoder thread for the frame the clock says we should be showing, if it is not already coming.
static void _requestFrame(VideoPlayerT *v) {
SDL_LockMutex(v->lock);
if ((v->frame != v->front.frame) && (v->frame != v->pendingFrame)) {
v->requestedFrame = v->frame;
v->pendingFrame = v->frame;
SDL_CondSignal(v->wake);
}
SDL_UnlockMutex(v->lock);
}
@ -395,7 +533,6 @@ static int64_t _msToSamples(VideoPlayerT *v, int64_t ms) {
static void _resetClock(VideoPlayerT *v, uint32_t now) {
v->startTicks = now;
v->startTime = _frameTime(v, v->frame);
v->frameDirty = true;
v->resetTime = false;
if (v->audioSourceCount > 0) {
SDL_LockAudio();
@ -409,6 +546,40 @@ static void _resetClock(VideoPlayerT *v, uint32_t now) {
}
// Swaps in a finished frame from the decoder thread. Returns true when there is a new one.
static bool _takeDecodedFrame(VideoPlayerT *v) {
FrameBufferT temp;
bool taken = false;
SDL_LockMutex(v->lock);
if (v->threadError) {
utilDie("%s", v->threadErrInfo.Buffer);
}
if (v->backReady) {
temp = v->front;
v->front = v->back;
v->back = temp;
v->backReady = false;
taken = true;
SDL_CondSignal(v->wake);
}
SDL_UnlockMutex(v->lock);
return taken;
}
// Pushes the front buffer to the texture. YUV players convert on the GPU.
static void _uploadFrame(VideoPlayerT *v) {
if (v->rgb) {
SDL_UpdateTexture(v->videoTexture, NULL, v->front.data[0], v->front.linesize[0]);
} else {
SDL_UpdateYUVTexture(v->videoTexture, NULL, v->front.data[0], v->front.linesize[0], v->front.data[1], v->front.linesize[1], v->front.data[2], v->front.linesize[2]);
}
v->uploadedFrame = v->front.frame;
}
int32_t videoGetAudioTrack(int32_t playerHandle) {
return _getPlayer(playerHandle, "videoGetAudioTrack")->currentAudioTrack;
}
@ -465,33 +636,49 @@ const char *videoGetLanguageDescription(const char *languageCode) {
}
// Reads one pixel of the most recently decoded frame. Returns false if there is no frame yet.
// Reads one pixel of the frame being shown. Returns false if there is no frame yet.
bool videoGetPixel(int32_t playerHandle, int32_t x, int32_t y, uint8_t *r, uint8_t *g, uint8_t *b) {
VideoPlayerT *v = _getPlayer(playerHandle, "videoGetPixel");
const uint8_t *pixel = NULL;
int32_t c = 0;
int32_t d = 0;
int32_t e = 0;
int32_t value = 0;
if ((v->frameData == NULL) || (x < 0) || (y < 0) || (x >= v->width) || (y >= v->height)) {
if ((v->front.frame < 0) || (x < 0) || (y < 0) || (x >= v->width) || (y >= v->height)) {
return false;
}
// Frames are decoded as BGRA.
pixel = v->frameData->Data[0] + (y * v->frameData->Linesize[0]) + (x * BYTES_PER_PIXEL);
*b = pixel[0];
*g = pixel[1];
*r = pixel[2];
if (v->rgb) {
pixel = v->front.data[0] + (y * v->front.linesize[0]) + (x * BYTES_PER_PIXEL);
*b = pixel[0];
*g = pixel[1];
*r = pixel[2];
} else {
// BT.601 limited range, the same conversion SDL applies on the GPU for SD video.
c = v->front.data[0][y * v->front.linesize[0] + x] - 16;
d = v->front.data[1][(y / 2) * v->front.linesize[1] + (x / 2)] - 128;
e = v->front.data[2][(y / 2) * v->front.linesize[2] + (x / 2)] - 128;
value = (298 * c + 409 * e + 128) >> 8;
*r = (uint8_t)((value < 0) ? 0 : ((value > 255) ? 255 : value));
value = (298 * c - 100 * d - 208 * e + 128) >> 8;
*g = (uint8_t)((value < 0) ? 0 : ((value > 255) ? 255 : value));
value = (298 * c + 516 * d + 128) >> 8;
*b = (uint8_t)((value < 0) ? 0 : ((value > 255) ? 255 : value));
}
return true;
}
// Exposes the most recently decoded BGRA frame. Valid until the next videoUpdate of this player.
// Exposes the BGRA frame being shown (players loaded with rgb). Valid until the next videoUpdate of this player.
bool videoGetPixels(int32_t playerHandle, const uint8_t **pixels, int32_t *pitch) {
VideoPlayerT *v = _getPlayer(playerHandle, "videoGetPixels");
if (v->frameData == NULL) {
if (!v->rgb || (v->front.frame < 0)) {
return false;
}
*pixels = v->frameData->Data[0];
*pitch = v->frameData->Linesize[0];
*pixels = v->front.data[0];
*pitch = v->front.linesize[0];
return true;
}
@ -543,8 +730,9 @@ bool videoIsPlaying(int32_t playerHandle) {
}
// audioFilename may be NULL when the audio lives in the video file.
int32_t videoLoad(const char *videoFilename, const char *audioFilename, const char *indexPath, SDL_Renderer *renderer) {
// audioFilename may be NULL when the audio lives in the video file. rgb players decode to BGRA so
// scripts can read the pixels; everything else stays YUV and is converted by the GPU.
int32_t videoLoad(const char *videoFilename, const char *audioFilename, const char *indexPath, SDL_Renderer *renderer, bool rgb) {
int32_t pixelFormats[2];
FFMS_Index *vIndex = NULL;
FFMS_Index *aIndex = NULL;
@ -568,6 +756,12 @@ int32_t videoLoad(const char *videoFilename, const char *audioFilename, const ch
v->errInfo.BufferSize = sizeof(v->errMsg);
v->errInfo.ErrorType = FFMS_ERROR_SUCCESS;
v->errInfo.SubType = FFMS_ERROR_SUCCESS;
v->threadErrInfo = v->errInfo;
v->threadErrInfo.Buffer = v->threadErrMsg;
v->rgb = rgb;
v->requestedFrame = -1;
v->pendingFrame = -1;
v->uploadedFrame = -1;
if (audioFilename) {
vIndex = _createIndex(videoFilename, indexPath, true, false, v);
@ -599,12 +793,13 @@ int32_t videoLoad(const char *videoFilename, const char *audioFilename, const ch
v->width = frame->EncodedWidth;
v->height = frame->EncodedHeight;
// Set up output video format
pixelFormats[0] = FFMS_GetPixFmt("bgra");
// Set up output video format. YUV 4:2:0 is what the decoder produces, so no conversion runs.
pixelFormats[0] = FFMS_GetPixFmt(rgb ? "bgra" : "yuv420p");
pixelFormats[1] = -1;
if (FFMS_SetOutputFormatV2(v->videoSource, pixelFormats, v->width, v->height, FFMS_RESIZER_BICUBIC, &v->errInfo)) {
utilDie("%s", v->errInfo.Buffer);
}
_reportKeyframes(v, videoFilename);
// Find audio track(s)
_loadAudio(v, audioFilename, aIndex);
@ -616,11 +811,24 @@ int32_t videoLoad(const char *videoFilename, const char *audioFilename, const ch
FFMS_DestroyIndex(vIndex);
// Create video texture
v->videoTexture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_BGRA32, SDL_TEXTUREACCESS_STREAMING, v->width, v->height);
v->videoTexture = SDL_CreateTexture(renderer, rgb ? SDL_PIXELFORMAT_BGRA32 : SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, v->width, v->height);
if (v->videoTexture == NULL) {
utilDie("%s", SDL_GetError());
}
// Frame buffers and the decoder thread that fills them
_allocateFrameBuffer(v, &v->front);
_allocateFrameBuffer(v, &v->back);
v->lock = SDL_CreateMutex();
v->wake = SDL_CreateCond();
if (!v->lock || !v->wake) {
utilDie("%s", SDL_GetError());
}
v->thread = SDL_CreateThread(_decoderThread, "singeDecoder", v);
if (v->thread == NULL) {
utilDie("%s", SDL_GetError());
}
// Do we have audio?
if (v->audioSourceCount > 0) {
// Determine audio format
@ -800,6 +1008,19 @@ void videoUnload(int32_t playerHandle) {
free(v->audio);
}
// Stop the decoder before touching anything it uses.
SDL_LockMutex(v->lock);
v->quitThread = true;
SDL_CondSignal(v->wake);
SDL_UnlockMutex(v->lock);
SDL_WaitThread(v->thread, NULL);
SDL_DestroyCond(v->wake);
SDL_DestroyMutex(v->lock);
for (x = 0; x < PLANE_COUNT; x++) {
free(v->front.data[x]);
free(v->back.data[x]);
}
FFMS_DestroyVideoSource(v->videoSource);
SDL_DestroyTexture(v->videoTexture);
@ -833,8 +1054,7 @@ int64_t videoUpdate(int32_t playerHandle, SDL_Texture **texture) {
// Advance to the last frame whose presentation time has arrived.
next = v->frame + 1;
while ((next < count) && (_frameTime(v, next) <= elapsed)) {
v->frame = next;
v->frameDirty = true;
v->frame = next;
next++;
}
// Past the end of the last frame? Loop.
@ -848,8 +1068,10 @@ int64_t videoUpdate(int32_t playerHandle, SDL_Texture **texture) {
}
}
if (v->frameDirty) {
_loadFrame(v);
// Hand the decoder the frame we want, and show whatever it has finished.
_requestFrame(v);
if (_takeDecodedFrame(v)) {
_uploadFrame(v);
}
*texture = v->videoTexture;
@ -858,5 +1080,6 @@ int64_t videoUpdate(int32_t playerHandle, SDL_Texture **texture) {
_feedAudio(v);
}
return v->frame;
// Report what is on screen, so the caller redraws when it changes.
return v->uploadedFrame;
}

View file

@ -49,7 +49,7 @@ void videoGetVolume(int32_t playerHandle, int32_t *leftPercent, int32_t *
int32_t videoGetWidth(int32_t playerHandle);
void videoInit(int32_t mixerChunkFrames);
bool videoIsPlaying(int32_t playerHandle);
int32_t videoLoad(const char *videoFilename, const char *audioFilename, const char *indexPath, SDL_Renderer *renderer);
int32_t videoLoad(const char *videoFilename, const char *audioFilename, const char *indexPath, SDL_Renderer *renderer, bool rgb);
void videoPause(int32_t playerHandle);
void videoPlay(int32_t playerHandle);
void videoQuit(void);