Audio sync rewritten. Pause fixed. Video decoding performance improvements. Ability to have disc-less games.
This commit is contained in:
parent
f6d63b6eb0
commit
34d61d957a
11 changed files with 486 additions and 108 deletions
16
CHANGELOG
16
CHANGELOG
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -301,11 +301,21 @@ 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
|
||||
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
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
|||
42
src/main.c
42
src/main.c
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
91
src/singe.c
91
src/singe.c
|
|
@ -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,7 +4351,8 @@ 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
|
||||
// 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);
|
||||
|
|
@ -4335,13 +4360,20 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) {
|
|||
_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);
|
||||
_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);
|
||||
videoWidth = videoGetWidth(_global.videoHandle);
|
||||
videoHeight = videoGetHeight(_global.videoHandle);
|
||||
_global.canvasWidth = videoGetWidth(_global.videoHandle);
|
||||
_global.canvasHeight = videoGetHeight(_global.videoHandle);
|
||||
} else {
|
||||
_progTrace("No disc; canvas is %dx%d", _global.conf->canvasWidth, _global.conf->canvasHeight);
|
||||
_global.canvasWidth = _global.conf->canvasWidth;
|
||||
_global.canvasHeight = _global.conf->canvasHeight;
|
||||
}
|
||||
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).
|
||||
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,7 +4730,8 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) {
|
|||
}
|
||||
}
|
||||
|
||||
// Update video
|
||||
// Update the disc video
|
||||
if (_global.videoHandle >= 0) {
|
||||
thisFrame = videoUpdate(_global.videoHandle, &_global.videoTexture);
|
||||
if (_global.conf->isFrameFile) {
|
||||
frameFileUpdate(_global.frameFileHandle, &_global.videoHandle);
|
||||
|
|
@ -4707,6 +4742,7 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) {
|
|||
frameClock = 0;
|
||||
_global.refreshDisplay = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Call game code, unless the engine has it paused.
|
||||
if (!_global.frozen && (SDL_GetTicks() > frameClock)) {
|
||||
|
|
@ -4735,7 +4771,8 @@ 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
|
||||
// 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);
|
||||
|
|
@ -4743,6 +4780,7 @@ void singe(SDL_Window *window, SDL_Renderer *renderer, ConfigT *conf) {
|
|||
} else {
|
||||
SDL_RenderCopy(_global.renderer, _global.videoTexture, NULL, &windowTarget);
|
||||
}
|
||||
}
|
||||
// Overlay
|
||||
if (_global.overlayDirty) {
|
||||
SDL_UpdateTexture(_global.overlayTexture, NULL, _global.overlay->pixels, _global.overlay->pitch);
|
||||
|
|
@ -4806,12 +4844,15 @@ 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 {
|
||||
if (_global.videoHandle >= 0) {
|
||||
_progTrace("Unloading main video file");
|
||||
videoUnload(_global.videoHandle);
|
||||
}
|
||||
}
|
||||
|
||||
// Stop controllers
|
||||
_progTrace("Stopping controllers");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,27 +482,57 @@ 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);
|
||||
}
|
||||
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
// Restart the presentation clock at the current frame and realign audio to it.
|
||||
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);
|
||||
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);
|
||||
|
||||
|
|
@ -834,7 +1055,6 @@ int64_t videoUpdate(int32_t playerHandle, SDL_Texture **texture) {
|
|||
next = v->frame + 1;
|
||||
while ((next < count) && (_frameTime(v, next) <= elapsed)) {
|
||||
v->frame = next;
|
||||
v->frameDirty = true;
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue